/** * Distribution License: * JSword is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License, version 2.1 as published by * the Free Software Foundation. This program is distributed in the hope * that it will be useful, but WITHOUT ANY WARRANTY; without even the * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * The License is available on the internet at: * http://www.gnu.org/copyleft/lgpl.html * or by writing to: * Free Software Foundation, Inc. * 59 Temple Place - Suite 330 * Boston, MA 02111-1307, USA * * Copyright: 2005 * The copyright to this program is held by it's authors. * * ID: $Id: ConfigEntryTable.java 2099 2011-03-07 17:13:00Z dmsmith $ */ package org.crosswire.jsword.book.sword; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.crosswire.common.util.Language; import org.crosswire.common.util.Languages; import org.crosswire.common.util.Logger; import org.crosswire.common.util.Reporter; import org.crosswire.jsword.JSMsg; import org.crosswire.jsword.book.BookCategory; import org.crosswire.jsword.book.OSISUtil; import org.jdom.Element; /** * 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*(.*)$"); }