/** * Provides different ways to sort Books. * * @see gnu.lgpl.License for license details.<br> * The copyright to this program is held by it's authors. * @author DM Smith [dmsmith555 at yahoo dot com] */ public final class BookComparators { /** Ensure we can't be created */ private BookComparators() {} /** Order by default Book ordering */ public static Comparator<Book> getDefault() { return new Comparator<Book>() { public int compare(Book o1, Book o2) { return o1.compareTo(o2); } }; } /** Order by Initials. */ public static Comparator<Book> getInitialComparator() { return new Comparator<Book>() { public int compare(Book o1, Book o2) { return o1.getInitials().compareTo(o2.getInitials()); } }; } /** The log stream */ static final Logger log = Logger.getLogger(BookComparators.class); }
/** * A class to convert between strings and objects of a type. * * @see gnu.lgpl.License for license details.<br> * The copyright to this program is held by it's authors. * @author Joe Walker [joe at eireneh dot com] */ public class ClassChoice extends AbstractReflectedChoice { /* * (non-Javadoc) * * @see org.crosswire.common.config.Choice#getConvertionClass() */ public Class<?> getConversionClass() { return Class.class; } /* * (non-Javadoc) * * @see * org.crosswire.common.config.AbstractReflectedChoice#convertToString(java * .lang.Object) */ @Override public String convertToString(Object orig) { if (orig == null) { return null; } return ((Class<?>) orig).getName(); } /* * (non-Javadoc) * * @see * org.crosswire.common.config.AbstractReflectedChoice#convertToObject(java * .lang.String) */ @Override public Object convertToObject(String orig) { try { return ClassUtil.forName(orig); } catch (ClassNotFoundException ex) { log.warn("Class not found: " + orig, ex); return null; } } /** The log stream */ private static final Logger log = Logger.getLogger(ClassChoice.class); }
/** * . * * @see gnu.lgpl.License for license details.<br> * The copyright to this program is held by it's authors. * @author Joe Walker [joe at eireneh dot com] * @author DM Smith [dmsmith555 at yahoo dot com] */ public abstract class AbstractSwordInstaller extends AbstractBookList implements Installer, Comparable<AbstractSwordInstaller> { /** * Utility to download a file from a remote site * * @param job The way of noting progress * @param dir The directory from which to download the file * @param file The file to download * @throws InstallException */ protected abstract void download(Progress job, String dir, String file, URI dest) throws InstallException; /* * (non-Javadoc) * * @see org.crosswire.jsword.book.install.Installer#getInstallerDefinition() */ public String getInstallerDefinition() { StringBuilder buf = new StringBuilder(host); buf.append(','); buf.append(packageDirectory); buf.append(','); buf.append(catalogDirectory); buf.append(','); buf.append(indexDirectory); buf.append(','); if (proxyHost != null) { buf.append(proxyHost); } buf.append(','); if (proxyPort != null) { buf.append(proxyPort); } return buf.toString(); } /* * (non-Javadoc) * * @see * org.crosswire.jsword.book.install.Installer#isNewer(org.crosswire.jsword * .book.BookMetaData) */ public boolean isNewer(Book book) { File dldir = SwordBookPath.getSwordDownloadDir(); SwordBookMetaData sbmd = (SwordBookMetaData) book.getBookMetaData(); File conf = new File(dldir, sbmd.getConfPath()); // The conf may not exist in our download dir. // In this case we say that it should not be downloaded again. if (!conf.exists()) { return false; } URI configURI = NetUtil.getURI(conf); URI remote = toRemoteURI(book); return NetUtil.isNewer(remote, configURI, proxyHost, proxyPort); } /* * (non-Javadoc) * * @see org.crosswire.jsword.book.BookList#getBooks() */ public List<Book> getBooks() { try { if (!loaded) { loadCachedIndex(); } // We need to create a List from the Set returned by // entries.values() so the underlying list is not modified. return new ArrayList<Book>(entries.values()); } catch (InstallException ex) { log.error("Failed to reload cached index file", ex); return new ArrayList<Book>(); } } /* * (non-Javadoc) * * @see org.crosswire.jsword.book.BookList#getBook(java.lang.String) */ public synchronized Book getBook(String name) { // Check name first // First check for exact matches for (Book book : getBooks()) { if (name.equals(book.getName())) { return book; } } // Next check for case-insensitive matches for (Book book : getBooks()) { if (name.equalsIgnoreCase(book.getName())) { return book; } } // Then check initials // First check for exact matches for (Book book : getBooks()) { BookMetaData bmd = book.getBookMetaData(); if (name.equals(bmd.getInitials())) { return book; } } // Next check for case-insensitive matches for (Book book : getBooks()) { if (name.equalsIgnoreCase(book.getInitials())) { return book; } } return null; } /* * (non-Javadoc) * * @see * org.crosswire.jsword.book.BookList#getBooks(org.crosswire.jsword.book * .BookFilter) */ @Override public synchronized List<Book> getBooks(BookFilter filter) { List<Book> temp = CollectionUtil.createList(new BookFilterIterator(getBooks(), filter)); return new BookSet(temp); } /* * (non-Javadoc) * * @see * org.crosswire.jsword.book.install.Installer#install(org.crosswire.jsword * .book.Book) */ public void install(Book book) { // // Is the book already installed? Then nothing to do. // if (Books.installed().getBook(book.getName()) != null) // { // return; // } // final SwordBookMetaData sbmd = (SwordBookMetaData) book.getBookMetaData(); // So now we know what we want to install - all we need to do // is installer.install(name) however we are doing it in the // background so we create a job for it. final Thread worker = new Thread("DisplayPreLoader") { /* * (non-Javadoc) * * @see java.lang.Runnable#run() */ @Override public void run() { // TRANSLATOR: Progress label indicating the installation of a book. {0} is a // placeholder for the name of the book. String jobName = JSMsg.gettext("Installing book: {0}", sbmd.getName()); Progress job = JobManager.createJob(jobName, this); // Don't bother setting a size, we'll do it later. job.beginJob(jobName); yield(); URI temp = null; try { // TRANSLATOR: Progress label indicating the Initialization of installing of a book. job.setSectionName(JSMsg.gettext("Initializing")); temp = NetUtil.getTemporaryURI("swd", ZIP_SUFFIX); download(job, packageDirectory, sbmd.getInitials() + ZIP_SUFFIX, temp); // Once the unzipping is started, we need to continue job.setCancelable(false); if (!job.isFinished()) { File dldir = SwordBookPath.getSwordDownloadDir(); IOUtil.unpackZip(NetUtil.getAsFile(temp), dldir); // TRANSLATOR: Progress label for installing the conf file for a book. job.setSectionName(JSMsg.gettext("Copying config file")); sbmd.setLibrary(NetUtil.getURI(dldir)); SwordBookDriver.registerNewBook(sbmd); } } catch (IOException e) { Reporter.informUser(this, e); job.cancel(); } catch (InstallException e) { Reporter.informUser(this, e); job.cancel(); } catch (BookException e) { Reporter.informUser(this, e); job.cancel(); } finally { job.done(); // tidy up after ourselves // This is a best effort. If for some reason it does not delete now // it will automatically be deleted when the JVM exits normally. if (temp != null) { try { NetUtil.delete(temp); } catch (IOException e) { log.warn("Error deleting temp download file:" + e.getMessage()); } } } } }; // this actually starts the thread off worker.setPriority(Thread.MIN_PRIORITY); worker.start(); } /* * (non-Javadoc) * * @see org.crosswire.jsword.book.install.Installer#reloadIndex() */ public void reloadBookList() throws InstallException { // TRANSLATOR: Progress label for downloading one or more files. String jobName = JSMsg.gettext("Downloading files"); Progress job = JobManager.createJob(jobName, Thread.currentThread()); job.beginJob(jobName); try { URI scratchfile = getCachedIndexFile(); download(job, catalogDirectory, FILE_LIST_GZ, scratchfile); loaded = false; } catch (InstallException ex) { job.cancel(); throw ex; } finally { job.done(); } } /* * (non-Javadoc) * * @see * org.crosswire.jsword.book.install.Installer#downloadSearchIndex(org.crosswire * .jsword.book.BookMetaData, java.net.URI) */ public void downloadSearchIndex(Book book, URI localDest) throws InstallException { // TRANSLATOR: Progress label for downloading one or more files. String jobName = JSMsg.gettext("Downloading files"); Progress job = JobManager.createJob(jobName, Thread.currentThread()); job.beginJob(jobName); // MJD START // use and-bible index location String indexLocation = "/and-bible/indices/v1"; try { Version versionObj = (Version) book.getBookMetaData().getProperty("Version"); String version = versionObj == null ? null : versionObj.toString(); String versionSuffix = version != null ? "-" + version : ""; download(job, indexLocation, book.getInitials() + versionSuffix + ZIP_SUFFIX, localDest); // MJD END // download(job, packageDirectory + '/' + SEARCH_DIR, book.getInitials() + // ZIP_SUFFIX, localDest); } catch (InstallException ex) { job.cancel(); throw ex; } finally { job.done(); } } /** Load the cached index file into memory */ private void loadCachedIndex() throws InstallException { // We need a sword book driver so the installer can use the driver // name to use in deciding where to put the index. BookDriver fake = SwordBookDriver.instance(); entries.clear(); URI cache = getCachedIndexFile(); if (!NetUtil.isFile(cache)) { reloadBookList(); } InputStream in = null; GZIPInputStream gin = null; TarInputStream tin = null; try { ConfigEntry.resetStatistics(); in = NetUtil.getInputStream(cache); gin = new GZIPInputStream(in); tin = new TarInputStream(gin); while (true) { TarEntry entry = tin.getNextEntry(); if (entry == null) { break; } String internal = entry.getName(); if (!entry.isDirectory()) { try { int size = (int) entry.getSize(); // Every now and then an empty entry sneaks in if (size == 0) { log.error("Empty entry: " + internal); continue; } byte[] buffer = new byte[size]; if (tin.read(buffer) != size) { // This should not happen, but if it does then skip // it. log.error("Did not read all that was expected " + internal); continue; } if (internal.endsWith(SwordConstants.EXTENSION_CONF)) { internal = internal.substring(0, internal.length() - 5); } else { log.error("Not a SWORD config file: " + internal); continue; } if (internal.startsWith(SwordConstants.DIR_CONF + '/')) { internal = internal.substring(7); } SwordBookMetaData sbmd = new SwordBookMetaData(buffer, internal); sbmd.setDriver(fake); Book book = new SwordBook(sbmd, null); entries.put(book.getName(), book); } catch (IOException ex) { log.error("Failed to load config for entry: " + internal, ex); } } } loaded = true; ConfigEntry.dumpStatistics(); } catch (IOException ex) { throw new InstallException(JSOtherMsg.lookupText("Error loading from cache"), ex); } finally { IOUtil.close(tin); IOUtil.close(gin); IOUtil.close(in); } } // MJD START - allow list to be cleared to clear a large block of memory @Override public void close() { entries.clear(); loaded = false; } // MJD END /** @return the catologDirectory */ public String getCatalogDirectory() { return catalogDirectory; } /** @param catologDirectory the catologDirectory to set */ public void setCatalogDirectory(String catologDirectory) { this.catalogDirectory = catologDirectory; } /** @return Returns the directory. */ public String getPackageDirectory() { return packageDirectory; } /** @param newDirectory The directory to set. */ public void setPackageDirectory(String newDirectory) { if (packageDirectory == null || !packageDirectory.equals(newDirectory)) { packageDirectory = newDirectory; loaded = false; } } /** @return the indexDirectory */ public String getIndexDirectory() { return indexDirectory; } /** @param indexDirectory the indexDirectory to set */ public void setIndexDirectory(String indexDirectory) { this.indexDirectory = indexDirectory; } /** @return Returns the host. */ public String getHost() { return host; } /** @param newHost The host to set. */ public void setHost(String newHost) { if (host == null || !host.equals(newHost)) { host = newHost; loaded = false; } } /** @return Returns the proxyHost. */ public String getProxyHost() { return proxyHost; } /** @param newProxyHost The proxyHost to set. */ public void setProxyHost(String newProxyHost) { String pHost = null; if (newProxyHost != null && newProxyHost.length() > 0) { pHost = newProxyHost; } if (proxyHost == null || !proxyHost.equals(pHost)) { proxyHost = pHost; loaded = false; } } /** @return Returns the proxyPort. */ public Integer getProxyPort() { return proxyPort; } /** @param newProxyPort The proxyPort to set. */ public void setProxyPort(Integer newProxyPort) { if (proxyPort == null || !proxyPort.equals(newProxyPort)) { proxyPort = newProxyPort; loaded = false; } } /** The URL for the cached index file for this installer */ protected URI getCachedIndexFile() throws InstallException { try { URI scratchdir = CWProject.instance() .getWriteableProjectSubdir(getTempFileExtension(host, catalogDirectory), true); return NetUtil.lengthenURI(scratchdir, FILE_LIST_GZ); } catch (IOException ex) { throw new InstallException(JSOtherMsg.lookupText("URL manipulation failed"), ex); } } /** What are we using as a temp filename? */ private static String getTempFileExtension(String host, String catalogDir) { return DOWNLOAD_PREFIX + host + catalogDir.replace('/', '_'); } /* * (non-Javadoc) * * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object object) { if (!(object instanceof AbstractSwordInstaller)) { return false; } AbstractSwordInstaller that = (AbstractSwordInstaller) object; if (!equals(this.host, that.host)) { return false; } if (!equals(this.packageDirectory, that.packageDirectory)) { return false; } return true; } /* * (non-Javadoc) * * @see java.lang.Comparable#compareTo(java.lang.Object) */ public int compareTo(AbstractSwordInstaller myClass) { int ret = host.compareTo(myClass.host); if (ret != 0) { ret = packageDirectory.compareTo(myClass.packageDirectory); } return ret; } /* * (non-Javadoc) * * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return host.hashCode() + packageDirectory.hashCode(); } /** Quick utility to check to see if 2 (potentially null) strings are equal */ protected boolean equals(String string1, String string2) { if (string1 == null) { return string2 == null; } return string1.equals(string2); } /** A map of the books in this download area */ protected Map<String, Book> entries = new HashMap<String, Book>(); /** The remote hostname. */ protected String host; /** The remote proxy hostname. */ protected String proxyHost; /** The remote proxy port. */ protected Integer proxyPort; /** The directory containing zipped books on the <code>host</code>. */ protected String packageDirectory = ""; /** The directory containing the catalog of all books on the <code>host</code>. */ protected String catalogDirectory = ""; /** The directory containing the catalog of all books on the <code>host</code>. */ protected String indexDirectory = ""; /** Do we need to reload the index file */ protected boolean loaded; /** The sword index file */ protected static final String FILE_LIST_GZ = "mods.d.tar.gz"; /** The suffix of zip books on this server */ protected static final String ZIP_SUFFIX = ".zip"; /** The log stream */ protected static final Logger log = Logger.getLogger(AbstractSwordInstaller.class); /** The relative path of the dir holding the search index files */ protected static final String SEARCH_DIR = "search/jsword/L1"; /** When we cache a download index */ protected static final String DOWNLOAD_PREFIX = "download-"; }
/** * A utility class for loading the entries in a Sword book's conf file. Since the conf files are * manually maintained, there can be all sorts of errors in them. This class does robust checking * and reporting. * * <p>Config file format. See also: <a href= * "http://sword.sourceforge.net/cgi-bin/twiki/view/Swordapi/ConfFileLayout"> * http://sword.sourceforge.net/cgi-bin/twiki/view/Swordapi/ConfFileLayout</a> * * <p>The contents of the About field are in rtf. * * <p>\ is used as a continuation line. * * @see gnu.lgpl.License for license details.<br> * The copyright to this program is held by it's authors. * @author Mark Goodwin [mark at thorubio dot org] * @author Joe Walker [joe at eireneh dot com] * @author Jacky Cheung * @author DM Smith [dmsmith555 at yahoo dot com] */ public final class ConfigEntryTable { /** * Create an empty Sword config for the named book. * * @param bookName the name of the book */ public ConfigEntryTable(String bookName) { table = new HashMap<ConfigEntryType, ConfigEntry>(); extra = new TreeMap<String, ConfigEntry>(); internal = bookName; supported = true; } private static long MAX_BUFF_SIZE = 8 * 1024; private static int MIN_BUFF_SIZE = 128; /** * Load the conf from a file. * * @param file the file to load * @throws IOException */ public void load(File file) throws IOException { configFile = file; BufferedReader in = null; try { // MJD start get best buffersize but ensure it is not too small (0) nor too large (>default) int bufferSize = (int) Math.min(MAX_BUFF_SIZE, file.length()); bufferSize = Math.max(MIN_BUFF_SIZE, bufferSize); // Quiet Android from complaining about using the default BufferReader buffer size. // The actual buffer size is undocumented. So this is a good idea any way. in = new BufferedReader( new InputStreamReader(new FileInputStream(file), ENCODING_UTF8), bufferSize); // MJD end loadInitials(in); loadContents(in); in.close(); in = null; if (getValue(ConfigEntryType.ENCODING).equals(ENCODING_LATIN1)) { supported = true; bookType = null; questionable = false; readahead = null; table.clear(); extra.clear(); in = new BufferedReader( new InputStreamReader(new FileInputStream(file), ENCODING_LATIN1), bufferSize); loadInitials(in); loadContents(in); in.close(); in = null; } adjustDataPath(); adjustLanguage(); adjustBookType(); adjustName(); validate(); } finally { if (in != null) { in.close(); } } } /** * Load the conf from a buffer. This is used to load conf entries from the mods.d.tar.gz file. * * @param buffer the buffer to load * @throws IOException */ public void load(byte[] buffer) throws IOException { BufferedReader in = null; try { // Quiet Android from complaining about using the default BufferReader buffer size. // The actual buffer size is undocumented. So this is a good idea any way. in = new BufferedReader( new InputStreamReader(new ByteArrayInputStream(buffer), ENCODING_UTF8), buffer.length); loadInitials(in); loadContents(in); in.close(); in = null; if (getValue(ConfigEntryType.ENCODING).equals(ENCODING_LATIN1)) { supported = true; bookType = null; questionable = false; readahead = null; table.clear(); extra.clear(); in = new BufferedReader( new InputStreamReader(new ByteArrayInputStream(buffer), ENCODING_LATIN1), buffer.length); loadInitials(in); loadContents(in); in.close(); in = null; } adjustDataPath(); adjustLanguage(); adjustBookType(); adjustName(); validate(); } finally { if (in != null) { in.close(); } } } /** Determines whether the Sword Book's conf is supported by JSword. */ public boolean isQuestionable() { return questionable; } /** Determines whether the Sword Book's conf is supported by JSword. */ public boolean isSupported() { return supported; } /** * Determines whether the Sword Book is enciphered. * * @return true if enciphered */ public boolean isEnciphered() { String cipher = (String) getValue(ConfigEntryType.CIPHER_KEY); return cipher != null; } /** * Determines whether the Sword Book is enciphered and without a key. * * @return true if enciphered */ public boolean isLocked() { String cipher = (String) getValue(ConfigEntryType.CIPHER_KEY); return cipher != null && cipher.length() == 0; } /** * Unlocks a book with the given key. The key is trimmed of any leading or trailing whitespace. * * @param unlockKey the key to try * @return true if the unlock key worked. */ public boolean unlock(String unlockKey) { String tmpKey = unlockKey; if (tmpKey != null) { tmpKey = tmpKey.trim(); } add(ConfigEntryType.CIPHER_KEY, tmpKey); if (configFile != null) { try { save(); } catch (IOException e) { // TRANSLATOR: Common error condition: The user supplied unlock key could not be saved. Reporter.informUser(this, JSMsg.gettext("Unable to save the book's unlock key.")); } } return true; } /** * Gets the unlock key for the module. * * @return the unlock key, if any, null otherwise. */ public String getUnlockKey() { return (String) getValue(ConfigEntryType.CIPHER_KEY); } /** Returns an Enumeration of all the known keys found in the config file. */ public Set<ConfigEntryType> getKeys() { return table.keySet(); } /** Returns an Enumeration of all the unknown keys found in the config file. */ public Set<String> getExtraKeys() { return extra.keySet(); } /** Returns an Enumeration of all the keys found in the config file. */ public BookType getBookType() { return bookType; } /** * Gets a particular ConfigEntry's value by its type * * @param type of the ConfigEntry * @return the requested value, the default (if there is no entry) or null (if there is no * default) */ public Object getValue(ConfigEntryType type) { ConfigEntry ce = table.get(type); if (ce != null) { return ce.getValue(); } return type.getDefault(); } /** * Determine whether this ConfigEntryTable has the ConfigEntry and it matches the value. * * @param type The kind of ConfigEntry to look for * @param search the value to match against * @return true if there is a matching ConfigEntry matching the value */ // MJD latest change in jsword public boolean match(ConfigEntryType type, String search) { ConfigEntry ce = table.get(type); return ce != null && ce.match(search); } /** Sort the keys for a more meaningful presentation order. */ public Element toOSIS() { OSISUtil.OSISFactory factory = OSISUtil.factory(); Element ele = factory.createTable(); toOSIS(factory, ele, "BasicInfo", BASIC_INFO); toOSIS(factory, ele, "LangInfo", LANG_INFO); toOSIS(factory, ele, "LicenseInfo", COPYRIGHT_INFO); toOSIS(factory, ele, "FeatureInfo", FEATURE_INFO); toOSIS(factory, ele, "SysInfo", SYSTEM_INFO); toOSIS(factory, ele, "Extra", extra); return ele; } /** * Build's a SWORD conf file as a string. The result is not identical to the original, cleaning up * problems in the original and re-arranging the entries into a predictable order. * * @return the well-formed conf. */ public String toConf() { StringBuilder buf = new StringBuilder(); buf.append('['); buf.append(getValue(ConfigEntryType.INITIALS)); buf.append("]\n"); toConf(buf, BASIC_INFO); toConf(buf, SYSTEM_INFO); toConf(buf, HIDDEN); toConf(buf, FEATURE_INFO); toConf(buf, LANG_INFO); toConf(buf, COPYRIGHT_INFO); toConf(buf, extra); return buf.toString(); } public void save() throws IOException { if (configFile != null) { // The encoding of the conf must match the encoding of the module. String encoding = ENCODING_LATIN1; if (getValue(ConfigEntryType.ENCODING).equals(ENCODING_UTF8)) { encoding = ENCODING_UTF8; } Writer writer = null; try { writer = new OutputStreamWriter(new FileOutputStream(configFile), encoding); writer.write(toConf()); } finally { if (writer != null) { writer.close(); } } } } public void save(File file) throws IOException { this.configFile = file; this.save(); } private void loadContents(BufferedReader in) throws IOException { StringBuilder buf = new StringBuilder(); while (true) { // Empty out the buffer buf.setLength(0); String line = advance(in); if (line == null) { break; } // skip blank lines if (line.length() == 0) { continue; } Matcher matcher = KEY_VALUE_PATTERN.matcher(line); if (!matcher.matches()) { log.warn("Expected to see '=' in " + internal + ": " + line); continue; } String key = matcher.group(1).trim(); String value = matcher.group(2).trim(); // Only CIPHER_KEYS that are empty are not ignored if (value.length() == 0 && !ConfigEntryType.CIPHER_KEY.getName().equals(key)) { log.warn("Ignoring empty entry in " + internal + ": " + line); continue; } // Create a configEntry so that the name is normalized. ConfigEntry configEntry = new ConfigEntry(internal, key); ConfigEntryType type = configEntry.getType(); ConfigEntry e = table.get(type); if (e == null) { if (type == null) { log.warn("Extra entry in " + internal + " of " + configEntry.getName()); extra.put(key, configEntry); } else if (type.isSynthetic()) { log.warn("Ignoring unexpected entry in " + internal + " of " + configEntry.getName()); } else { table.put(type, configEntry); } } else { configEntry = e; } buf.append(value); getContinuation(configEntry, in, buf); // History is a special case it is of the form History_x.x // The config entry is History without the x.x. // We want to put x.x at the beginning of the string value = buf.toString(); if (ConfigEntryType.HISTORY.equals(type)) { int pos = key.indexOf('_'); value = key.substring(pos + 1) + ' ' + value; } configEntry.addValue(value); } } private void loadInitials(BufferedReader in) throws IOException { String initials = null; while (true) { String line = advance(in); if (line == null) { break; } if (line.charAt(0) == '[' && line.charAt(line.length() - 1) == ']') { // The conf file contains a leading line of the form [KJV] // This is the acronym by which Sword refers to it. initials = line.substring(1, line.length() - 1); break; } } if (initials == null) { log.error( "Malformed conf file for " + internal + " no initials found. Using internal of " + internal); initials = internal; } add(ConfigEntryType.INITIALS, initials); } /** Get continuation lines, if any. */ private void getContinuation(ConfigEntry configEntry, BufferedReader bin, StringBuilder buf) throws IOException { for (String line = advance(bin); line != null; line = advance(bin)) { int length = buf.length(); // Look for bad data as this condition did exist boolean continuation_expected = length > 0 && buf.charAt(length - 1) == '\\'; if (continuation_expected) { // delete the continuation character buf.deleteCharAt(length - 1); } if (isKeyLine(line)) { if (continuation_expected) { log.warn(report("Continuation followed by key for", configEntry.getName(), line)); } backup(line); break; } else if (!continuation_expected) { log.warn(report("Line without previous continuation for", configEntry.getName(), line)); } if (!configEntry.allowsContinuation()) { log.warn(report("Ignoring unexpected additional line for", configEntry.getName(), line)); } else { if (continuation_expected) { buf.append('\n'); } buf.append(line); } } } /** * Get the next line from the input * * @param bin The reader to get data from * @return the next line * @throws IOException */ private String advance(BufferedReader bin) throws IOException { // Was something put back? If so, return it. if (readahead != null) { String line = readahead; readahead = null; return line; } // Get the next non-blank, non-comment line String trimmed = null; for (String line = bin.readLine(); line != null; line = bin.readLine()) { // Remove trailing whitespace trimmed = line.trim(); int length = trimmed.length(); // skip blank and comment lines if (length != 0 && trimmed.charAt(0) != '#') { return trimmed; } } return null; } /** Read too far ahead and need to return a line. */ private void backup(String oops) { if (oops.length() > 0) { readahead = oops; } else { // should never happen log.error("Backup an empty string for " + internal); } } /** Does this line of text represent a key/value pair? */ private boolean isKeyLine(String line) { return KEY_VALUE_PATTERN.matcher(line).matches(); } /** * A helper to create/replace a value for a given type. * * @param type * @param aValue */ public void add(ConfigEntryType type, String aValue) { table.put(type, new ConfigEntry(internal, type, aValue)); } private void adjustDataPath() { String datapath = (String) getValue(ConfigEntryType.DATA_PATH); if (datapath == null) { datapath = ""; } if (datapath.startsWith("./")) { datapath = datapath.substring(2); } add(ConfigEntryType.DATA_PATH, datapath); } private void adjustLanguage() { Language lang = (Language) getValue(ConfigEntryType.LANG); if (lang == null) { lang = Language.DEFAULT_LANG; add(ConfigEntryType.LANG, lang.toString()); } testLanguage(internal, lang); Language langFrom = (Language) getValue(ConfigEntryType.GLOSSARY_FROM); Language langTo = (Language) getValue(ConfigEntryType.GLOSSARY_TO); // If we have either langFrom or langTo, we are dealing with a glossary if (langFrom != null || langTo != null) { if (langFrom == null) { log.warn( "Missing data for " + internal + ". Assuming " + ConfigEntryType.GLOSSARY_FROM.getName() + '=' + Languages.DEFAULT_LANG_CODE); langFrom = Language.DEFAULT_LANG; add(ConfigEntryType.GLOSSARY_FROM, lang.getCode()); } testLanguage(internal, langFrom); if (langTo == null) { log.warn( "Missing data for " + internal + ". Assuming " + ConfigEntryType.GLOSSARY_TO.getName() + '=' + Languages.DEFAULT_LANG_CODE); langTo = Language.DEFAULT_LANG; add(ConfigEntryType.GLOSSARY_TO, lang.getCode()); } testLanguage(internal, langTo); // At least one of the two languages should match the lang entry if (!langFrom.equals(lang) && !langTo.equals(lang)) { log.error( "Data error in " + internal + ". Neither " + ConfigEntryType.GLOSSARY_FROM.getName() + " or " + ConfigEntryType.GLOSSARY_FROM.getName() + " match " + ConfigEntryType.LANG.getName()); } else if (!langFrom.equals(lang)) { // The LANG field should match the GLOSSARY_FROM field /* * log.error("Data error in " + internal + ". " + * ConfigEntryType.GLOSSARY_FROM.getName() + " (" * + langFrom.getCode() + ") does not match " + * ConfigEntryType.LANG.getName() + " (" + lang.getCode() + * ")"); */ lang = langFrom; add(ConfigEntryType.LANG, lang.getCode()); } } } private void adjustBookType() { // The book type represents the underlying category of book. // Fine tune it here. BookCategory focusedCategory = (BookCategory) getValue(ConfigEntryType.CATEGORY); questionable = focusedCategory == BookCategory.QUESTIONABLE; // From the config map, extract the important bean properties String modTypeName = (String) getValue(ConfigEntryType.MOD_DRV); if (modTypeName == null) { log.error( "Book not supported: malformed conf file for " + internal + " no " + ConfigEntryType.MOD_DRV.getName() + " found"); supported = false; return; } bookType = BookType.fromString(modTypeName); if (getBookType() == null) { log.error("Book not supported: malformed conf file for " + internal + " no book type found"); supported = false; return; } BookCategory basicCategory = getBookType().getBookCategory(); if (basicCategory == null) { supported = false; return; } // The book type represents the underlying category of book. // Fine tune it here. if (focusedCategory == BookCategory.OTHER || focusedCategory == BookCategory.QUESTIONABLE) { focusedCategory = getBookType().getBookCategory(); } add(ConfigEntryType.CATEGORY, focusedCategory.getName()); } private void adjustName() { // If there is no name then use the internal name if (table.get(ConfigEntryType.DESCRIPTION) == null) { log.error( "Malformed conf file for " + internal + " no " + ConfigEntryType.DESCRIPTION.getName() + " found. Using internal of " + internal); add(ConfigEntryType.DESCRIPTION, internal); } } /** Determine which books are not supported. Also, report on problems. */ private void validate() { // if (isEnciphered()) // { // log.debug("Book not supported: " + internal + " because it is locked and there is // no key."); // supported = false; // return; // } } private void testLanguage(String initials, Language lang) { if (!lang.isValidLanguage()) { log.warn("Unknown language " + lang.getCode() + " in book " + initials); } } /** Build an ordered map so that it displays in a consistent order. */ private void toOSIS( OSISUtil.OSISFactory factory, Element ele, String aTitle, ConfigEntryType[] category) { Element title = null; for (int i = 0; i < category.length; i++) { ConfigEntry entry = table.get(category[i]); Element configElement = null; if (entry != null) { configElement = entry.toOSIS(); } if (title == null && configElement != null) { // I18N(DMS): use aTitle to lookup translation. title = factory.createHeader(); title.addContent(aTitle); ele.addContent(title); } if (configElement != null) { ele.addContent(configElement); } } } private void toConf(StringBuilder buf, ConfigEntryType[] category) { for (int i = 0; i < category.length; i++) { ConfigEntry entry = table.get(category[i]); if (entry != null && !entry.getType().isSynthetic()) { String text = entry.toConf(); if (text != null && text.length() > 0) { buf.append(entry.toConf()); } } } } /** Build an ordered map so that it displays in a consistent order. */ private void toOSIS( OSISUtil.OSISFactory factory, Element ele, String aTitle, Map<String, ConfigEntry> map) { Element title = null; for (Map.Entry<String, ConfigEntry> mapEntry : map.entrySet()) { ConfigEntry entry = mapEntry.getValue(); Element configElement = null; if (entry != null) { configElement = entry.toOSIS(); } if (title == null && configElement != null) { // I18N(DMS): use aTitle to lookup translation. title = factory.createHeader(); title.addContent(aTitle); ele.addContent(title); } if (configElement != null) { ele.addContent(configElement); } } } private void toConf(StringBuilder buf, Map<String, ConfigEntry> map) { for (Map.Entry<String, ConfigEntry> mapEntry : map.entrySet()) { ConfigEntry entry = mapEntry.getValue(); String text = entry.toConf(); if (text != null && text.length() > 0) { buf.append(text); } } } private String report(String issue, String confEntryName, String line) { StringBuilder buf = new StringBuilder(100); buf.append(issue); buf.append(' '); buf.append(confEntryName); buf.append(" in "); buf.append(internal); buf.append(": "); buf.append(line); return buf.toString(); } /** * Sword only recognizes two encodings for its modules: UTF-8 and LATIN1 Sword uses MS Windows * cp1252 for Latin 1 not the standard. Arrgh! */ private static final String ENCODING_UTF8 = "UTF-8"; private static final String ENCODING_LATIN1 = "WINDOWS-1252"; /** * These are the elements that JSword requires. They are a superset of those that Sword requires. */ /* * For documentation purposes at this time. private static final * ConfigEntryType[] REQUIRED = { ConfigEntryType.INITIALS, * ConfigEntryType.DESCRIPTION, ConfigEntryType.CATEGORY, // may not be * present in conf ConfigEntryType.DATA_PATH, ConfigEntryType.MOD_DRV, }; */ private static final ConfigEntryType[] BASIC_INFO = { ConfigEntryType.INITIALS, ConfigEntryType.DESCRIPTION, ConfigEntryType.CATEGORY, ConfigEntryType.LCSH, ConfigEntryType.SWORD_VERSION_DATE, ConfigEntryType.VERSION, ConfigEntryType.HISTORY, ConfigEntryType.OBSOLETES, ConfigEntryType.INSTALL_SIZE, }; private static final ConfigEntryType[] LANG_INFO = { ConfigEntryType.LANG, ConfigEntryType.GLOSSARY_FROM, ConfigEntryType.GLOSSARY_TO, }; private static final ConfigEntryType[] COPYRIGHT_INFO = { ConfigEntryType.ABOUT, ConfigEntryType.SHORT_PROMO, ConfigEntryType.DISTRIBUTION_LICENSE, ConfigEntryType.DISTRIBUTION_NOTES, ConfigEntryType.DISTRIBUTION_SOURCE, ConfigEntryType.SHORT_COPYRIGHT, ConfigEntryType.COPYRIGHT, ConfigEntryType.COPYRIGHT_DATE, ConfigEntryType.COPYRIGHT_HOLDER, ConfigEntryType.COPYRIGHT_CONTACT_NAME, ConfigEntryType.COPYRIGHT_CONTACT_ADDRESS, ConfigEntryType.COPYRIGHT_CONTACT_EMAIL, ConfigEntryType.COPYRIGHT_CONTACT_NOTES, ConfigEntryType.COPYRIGHT_NOTES, ConfigEntryType.TEXT_SOURCE, }; private static final ConfigEntryType[] FEATURE_INFO = { ConfigEntryType.FEATURE, ConfigEntryType.GLOBAL_OPTION_FILTER, ConfigEntryType.FONT, }; private static final ConfigEntryType[] SYSTEM_INFO = { ConfigEntryType.DATA_PATH, ConfigEntryType.MOD_DRV, ConfigEntryType.SOURCE_TYPE, ConfigEntryType.BLOCK_TYPE, ConfigEntryType.BLOCK_COUNT, ConfigEntryType.COMPRESS_TYPE, ConfigEntryType.ENCODING, ConfigEntryType.MINIMUM_VERSION, ConfigEntryType.OSIS_VERSION, ConfigEntryType.OSIS_Q_TO_TICK, ConfigEntryType.DIRECTION, ConfigEntryType.KEY_TYPE, ConfigEntryType.DISPLAY_LEVEL, }; private static final ConfigEntryType[] HIDDEN = { ConfigEntryType.CIPHER_KEY, }; /** The log stream */ private static final Logger log = Logger.getLogger(ConfigEntryTable.class); /** * The original name of this config file from mods.d. This is only used for managing warnings and * errors */ private String internal; /** A map of lists of known config entries. */ private Map<ConfigEntryType, ConfigEntry> table; /** A map of lists of unknown config entries. */ private Map<String, ConfigEntry> extra; /** The BookType for this ConfigEntry */ private BookType bookType; /** True if this book's config type can be used by JSword. */ private boolean supported; /** True if this book is considered questionable. */ private boolean questionable; /** A helper for the reading of the conf file. */ private String readahead; /** If the module's config is tied to a file remember it so that it can be updated. */ private File configFile; /** * Pattern that matches a key=value. The key can contain ascii letters, numbers, underscore and * period. The key must begin at the beginning of the line. The = sign following the key may be * surrounded by whitespace. The value may contain anything, including an = sign. */ private static final Pattern KEY_VALUE_PATTERN = Pattern.compile("^([A-Za-z0-9_.]+)\\s*=\\s*(.*)$"); }