Coverage Report - org.crosswire.jsword.book.sword.SwordBookMetaData
 
Classes in this File Line Coverage Branch Coverage Complexity
SwordBookMetaData
0%
0/409
0%
0/197
3.479
SwordBookMetaData$1
0%
0/1
N/A
3.479
SwordBookMetaData$KeyFilter
0%
0/7
0%
0/2
3.479
 
 1  
 /**
 2  
  * Distribution License:
 3  
  * JSword is free software; you can redistribute it and/or modify it under
 4  
  * the terms of the GNU Lesser General Public License, version 2.1 or later
 5  
  * as published by the Free Software Foundation. This program is distributed
 6  
  * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
 7  
  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 8  
  * See the GNU Lesser General Public License for more details.
 9  
  *
 10  
  * The License is available on the internet at:
 11  
  *      http://www.gnu.org/copyleft/lgpl.html
 12  
  * or by writing to:
 13  
  *      Free Software Foundation, Inc.
 14  
  *      59 Temple Place - Suite 330
 15  
  *      Boston, MA 02111-1307, USA
 16  
  *
 17  
  * © CrossWire Bible Society, 2005 - 2016
 18  
  *
 19  
  */
 20  
 package org.crosswire.jsword.book.sword;
 21  
 
 22  
 import java.io.File;
 23  
 import java.io.IOException;
 24  
 import java.io.PrintWriter;
 25  
 import java.io.Writer;
 26  
 import java.net.URI;
 27  
 import java.util.ArrayList;
 28  
 import java.util.Arrays;
 29  
 import java.util.Collection;
 30  
 import java.util.Collections;
 31  
 import java.util.HashMap;
 32  
 import java.util.HashSet;
 33  
 import java.util.Iterator;
 34  
 import java.util.List;
 35  
 import java.util.Map;
 36  
 import java.util.Set;
 37  
 import java.util.regex.Pattern;
 38  
 
 39  
 import org.crosswire.common.util.Filter;
 40  
 import org.crosswire.common.util.IOUtil;
 41  
 import org.crosswire.common.util.IniSection;
 42  
 import org.crosswire.common.util.Language;
 43  
 import org.crosswire.common.util.NetUtil;
 44  
 import org.crosswire.common.util.PropertyMap;
 45  
 import org.crosswire.common.xml.XMLUtil;
 46  
 import org.crosswire.jsword.JSMsg;
 47  
 import org.crosswire.jsword.book.BookCategory;
 48  
 import org.crosswire.jsword.book.BookException;
 49  
 import org.crosswire.jsword.book.FeatureType;
 50  
 import org.crosswire.jsword.book.KeyType;
 51  
 import org.crosswire.jsword.book.MetaDataLocator;
 52  
 import org.crosswire.jsword.book.OSISUtil;
 53  
 import org.crosswire.jsword.book.basic.AbstractBookMetaData;
 54  
 import org.crosswire.jsword.book.filter.SourceFilter;
 55  
 import org.crosswire.jsword.book.filter.SourceFilterFactory;
 56  
 import org.crosswire.jsword.versification.system.Versifications;
 57  
 import org.jdom2.Document;
 58  
 import org.jdom2.Element;
 59  
 import org.slf4j.Logger;
 60  
 import org.slf4j.LoggerFactory;
 61  
 
 62  
 /**
 63  
  * A utility class for loading and representing Sword book configs.
 64  
  *
 65  
  * <p>
 66  
  * Config file format. See also: <a href=
 67  
  * "http://sword.sourceforge.net/cgi-bin/twiki/view/Swordapi/ConfFileLayout">
 68  
  * http://sword.sourceforge.net/cgi-bin/twiki/view/Swordapi/ConfFileLayout</a>
 69  
  * <p>
 70  
  * In addition, the SwordBookMetaData is hierarchical. The Level
 71  
  * indicates where the file originates from. The full hierarchy could be laid
 72  
  * out as followed:
 73  
  * 
 74  
  * <pre>
 75  
  *     - sword
 76  
  *         - jsword
 77  
  *            - front-end
 78  
  * </pre>
 79  
  * 
 80  
  * Various rules govern where attributes are read from. The general rule is that
 81  
  * the highest level (front-end write) will override values from the lowest
 82  
  * common denominator (sword). Various parts of the tree may be missing as the
 83  
  * files may not exist on disk. There are exceptions however and each method in
 84  
  * this file documents its behavior.
 85  
  *
 86  
  * <p>
 87  
  * The contents of the About field are in RTF.
 88  
  * <p>
 89  
  * \ is used as a continuation line.
 90  
  *
 91  
  * @see gnu.lgpl.License The GNU Lesser General Public License for details.
 92  
  * @author Mark Goodwin
 93  
  * @author Joe Walker
 94  
  * @author Jacky Cheung
 95  
  * @author DM Smith
 96  
  */
 97  
 public final class SwordBookMetaData extends AbstractBookMetaData {
 98  
     // These plus a few from BookMetaData are the only ones defined by SWORD
 99  
     // These ones from BookMetaData are:
 100  
     // KEY_VERSIFICATION
 101  
     // KEY_FONT
 102  
     // KEY_LANG
 103  
     // KEY_CATEGORY
 104  
     // Two others are for JSword and are defined in BookMetaData:
 105  
     // KEY_SCOPE
 106  
     // KEY_BOOKLIST
 107  
     public static final String KEY_ABBREVIATION = "Abbreviation";
 108  
     public static final String KEY_ABOUT = "About";
 109  
     public static final String KEY_BLOCK_COUNT = "BlockCount";
 110  
     public static final String KEY_BLOCK_TYPE = "BlockType";
 111  
     public static final String KEY_CASE_SENSITIVE_KEYS = "CaseSensitiveKeys";
 112  
     public static final String KEY_CIPHER_KEY = "CipherKey";
 113  
     public static final String KEY_COMPRESS_TYPE = "CompressType";
 114  
     public static final String KEY_COPYRIGHT = "Copyright";
 115  
     public static final String KEY_COPYRIGHT_CONTACT_ADDRESS = "CopyrightContactAddress";
 116  
     public static final String KEY_COPYRIGHT_CONTACT_EMAIL = "CopyrightContactEmail";
 117  
     public static final String KEY_COPYRIGHT_CONTACT_NAME = "CopyrightContactName";
 118  
     public static final String KEY_COPYRIGHT_CONTACT_NOTES = "CopyrightContactNotes";
 119  
     public static final String KEY_COPYRIGHT_DATE = "CopyrightDate";
 120  
     public static final String KEY_COPYRIGHT_HOLDER = "CopyrightHolder";
 121  
     public static final String KEY_COPYRIGHT_NOTES = "CopyrightNotes";
 122  
     public static final String KEY_DATA_PATH = "DataPath";
 123  
     public static final String KEY_DESCRIPTION = "Description";
 124  
     public static final String KEY_DIRECTION = "Direction";
 125  
     public static final String KEY_DISPLAY_LEVEL = "DisplayLevel";
 126  
     public static final String KEY_DISTRIBUTION_LICENSE = "DistributionLicense";
 127  
     public static final String KEY_DISTRIBUTION_NOTES = "DistributionNotes";
 128  
     public static final String KEY_DISTRIBUTION_SOURCE = "DistributionSource";
 129  
     public static final String KEY_ENCODING = "Encoding";
 130  
     public static final String KEY_FEATURE = "Feature";
 131  
     public static final String KEY_GLOBAL_OPTION_FILTER = "GlobalOptionFilter";
 132  
     public static final String KEY_SIGLUM1 = "Siglum1";
 133  
     public static final String KEY_SIGLUM2 = "Siglum2";
 134  
     public static final String KEY_SIGLUM3 = "Siglum3";
 135  
     public static final String KEY_SIGLUM4 = "Siglum4";
 136  
     public static final String KEY_SIGLUM5 = "Siglum5";
 137  
     public static final String KEY_GLOSSARY_FROM = "GlossaryFrom";
 138  
     public static final String KEY_GLOSSARY_TO = "GlossaryTo";
 139  
     public static final String KEY_HISTORY = "History";
 140  
     public static final String KEY_INSTALL_SIZE = "InstallSize";
 141  
     public static final String KEY_KEY_TYPE = "KeyType";
 142  
     public static final String KEY_LCSH = "LCSH";
 143  
     public static final String KEY_LOCAL_STRIP_FILTER = "LocalStripFilter";
 144  
     public static final String KEY_MINIMUM_VERSION = "MinimumVersion";
 145  
     public static final String KEY_MOD_DRV = "ModDrv";
 146  
     public static final String KEY_OBSOLETES = "Obsoletes";
 147  
     public static final String KEY_OSIS_Q_TO_TICK = "OSISqToTick";
 148  
     public static final String KEY_OSIS_VERSION = "OSISVersion";
 149  
     public static final String KEY_PREFERRED_CSS_XHTML = "PreferredCSSXHTML";
 150  
     public static final String KEY_SEARCH_OPTION = "SearchOption";
 151  
     public static final String KEY_SHORT_COPYRIGHT = "ShortCopyright";
 152  
     public static final String KEY_SHORT_PROMO = "ShortPromo";
 153  
     public static final String KEY_SOURCE_TYPE = "SourceType";
 154  
     public static final String KEY_STRONGS_PADDING = "StrongsPadding";
 155  
     public static final String KEY_SWORD_VERSION_DATE = "SwordVersionDate";
 156  
     public static final String KEY_TEXT_SOURCE = "TextSource";
 157  
     public static final String KEY_UNLOCK_URL = "UnlockURL";
 158  
     public static final String KEY_VERSION = "Version";
 159  
 
 160  
     // Some keys have defaults
 161  
     public static final Map<String, String> DEFAULTS;
 162  
     static {
 163  0
         Map<String, String> tempMap = new HashMap<String, String>();
 164  0
         tempMap.put(KEY_COMPRESS_TYPE, "LZSS");
 165  0
         tempMap.put(KEY_BLOCK_TYPE, "CHAPTER");
 166  0
         tempMap.put(KEY_BLOCK_COUNT, "200");
 167  0
         tempMap.put(KEY_KEY_TYPE, "TreeKey");
 168  0
         tempMap.put(KEY_VERSIFICATION, "KJV");
 169  0
         tempMap.put(KEY_DIRECTION, "LtoR");
 170  0
         tempMap.put(KEY_SOURCE_TYPE, "Plaintext");
 171  0
         tempMap.put(KEY_ENCODING, "Latin-1");
 172  0
         tempMap.put(KEY_DISPLAY_LEVEL, "1");
 173  0
         tempMap.put(KEY_OSIS_Q_TO_TICK, "true");
 174  0
         tempMap.put(KEY_VERSION, "1.0");
 175  0
         tempMap.put(KEY_MINIMUM_VERSION, "1.5.1a");
 176  0
         tempMap.put(KEY_CATEGORY, "Other");
 177  0
         tempMap.put(KEY_LANG, "en");
 178  0
         tempMap.put(KEY_DISTRIBUTION_LICENSE, "Public Domain");
 179  0
         tempMap.put(KEY_CASE_SENSITIVE_KEYS, "false");
 180  0
         tempMap.put(KEY_STRONGS_PADDING, "true");
 181  0
         DEFAULTS = Collections.unmodifiableMap(tempMap);
 182  
     }
 183  
 
 184  
     /**
 185  
      * Loads a sword config from a given File.
 186  
      *
 187  
      * @param file
 188  
      *            the config file
 189  
      * @param bookRootPath
 190  
      *            the root path for the book
 191  
      * @throws IOException
 192  
      * @throws BookException
 193  
      *             indicates missing data files
 194  
      */
 195  0
     public SwordBookMetaData(File file, URI bookRootPath) throws IOException, BookException {
 196  0
         this.installed = true;
 197  0
         this.configFile = file;
 198  0
         this.bookConf = file.getName(); // something like kjv.conf
 199  0
         setLibrary(bookRootPath);
 200  
 
 201  0
         this.configAll = new IniSection();
 202  0
         this.filtered = true; // Force it to run.
 203  0
         reload(keyKeepers);
 204  0
     }
 205  
 
 206  
     /**
 207  
      * Loads a sword config from a buffer gotten from mods.d.tar.gz or mods.d.zip.
 208  
      *
 209  
      * @param buffer
 210  
      * @param bookConf
 211  
      * @throws IOException
 212  
      * @throws BookException
 213  
      */
 214  0
     public SwordBookMetaData(byte[] buffer, String bookConf) throws IOException, BookException {
 215  0
         this.installed = false;
 216  0
         this.bookConf = bookConf; // something like .../mods.d.zip!mods.d/kjv.conf
 217  0
         this.supported = true;
 218  0
         this.configAll = new IniSection();
 219  0
         this.filtered = true; // Force it to run.
 220  0
         loadBuffer(buffer, keyKeepers);
 221  0
         adjustConfig();
 222  0
         report(configAll);
 223  0
     }
 224  
 
 225  
     /* (non-Javadoc)
 226  
      * @see org.crosswire.jsword.book.basic.AbstractBookMetaData#reload()
 227  
      */
 228  
     @Override
 229  
     public void reload() throws BookException {
 230  0
         reload(null);
 231  0
     }
 232  
 
 233  
     public void reload(Filter<String> keepers) throws BookException {
 234  
         // Always run if it is filtered
 235  
         // Always run if a filter is supplied
 236  
         // Do not run if it is already not filtered and no filter is supplied
 237  0
         if (!filtered && keepers == null) {
 238  0
             return;
 239  
         }
 240  0
         this.supported = true;
 241  0
         if (configJSword != null) {
 242  0
             configJSword.clear();
 243  
         }
 244  0
         if (configFrontend != null) {
 245  0
             configFrontend.clear();
 246  
         }
 247  
         try {
 248  0
             if (installed) {
 249  0
                 loadFile(keepers);
 250  
             } else {
 251  0
                 byte[] buffer = IOUtil.getZipEntry(bookConf);
 252  0
                 loadBuffer(buffer, keepers);
 253  
             }
 254  0
             adjustConfig();
 255  0
             report(configAll);
 256  
 
 257  0
             this.configJSword = addConfig(MetaDataLocator.JSWORD);
 258  0
             this.configFrontend = addConfig(MetaDataLocator.FRONTEND);
 259  0
         } catch (IOException ex) {
 260  0
             throw new BookException("unable to load conf", ex);
 261  0
         }
 262  0
     }
 263  
 
 264  
     /* (non-Javadoc)
 265  
      * @see org.crosswire.jsword.book.BookMetaData#isQuestionable()
 266  
      */
 267  
     @Override
 268  
     public boolean isQuestionable() {
 269  
         // some parameters don't support overrides
 270  0
         return questionable;
 271  
     }
 272  
 
 273  
     /* (non-Javadoc)
 274  
     * @see org.crosswire.jsword.book.basic.AbstractBookMetaData#isSupported()
 275  
     */
 276  
     @Override
 277  
     public boolean isSupported() {
 278  
         // The top most states whether the Book is supported
 279  0
         return supported;
 280  
     }
 281  
 
 282  
     /* (non-Javadoc)
 283  
     * @see org.crosswire.jsword.book.basic.AbstractBookMetaData#isEnciphered()
 284  
     */
 285  
     @Override
 286  
     public boolean isEnciphered() {
 287  0
         String cipher = getProperty(KEY_CIPHER_KEY);
 288  0
         return cipher != null;
 289  
     }
 290  
 
 291  
     /* (non-Javadoc)
 292  
      * @see org.crosswire.jsword.book.basic.AbstractBookMetaData#isLocked()
 293  
      */
 294  
     @Override
 295  
     public boolean isLocked() {
 296  
         // A locked book is enciphered but without a key.
 297  0
         String cipher = getProperty(KEY_CIPHER_KEY);
 298  0
         return cipher != null && cipher.length() == 0;
 299  
     }
 300  
 
 301  
     /* (non-Javadoc)
 302  
      * @see org.crosswire.jsword.book.basic.AbstractBookMetaData#unlock(java.lang.String)
 303  
      */
 304  
     @Override
 305  
     public boolean unlock(String unlockKey) {
 306  
         // Persist the unlock key so that all can see it
 307  0
         putProperty(KEY_CIPHER_KEY, unlockKey, false);
 308  0
         return true;
 309  
     }
 310  
 
 311  
     /*
 312  
      * Can be overridden by front-end/jsword
 313  
      * (non-Javadoc)
 314  
      * @see org.crosswire.jsword.book.basic.AbstractBookMetaData#getUnlockKey()
 315  
      */
 316  
     @Override
 317  
     public String getUnlockKey() {
 318  0
         return getProperty(KEY_CIPHER_KEY);
 319  
     }
 320  
 
 321  
     /*
 322  
      * Can be overridden by front-end/jsword
 323  
      * (non-Javadoc)
 324  
      * @see org.crosswire.jsword.book.BookMetaData#getName()
 325  
      */
 326  
     public String getName() {
 327  0
         return getProperty(KEY_DESCRIPTION);
 328  
     }
 329  
 
 330  
     /* (non-Javadoc)
 331  
      * @see org.crosswire.jsword.book.BookMetaData#getBookCharset()
 332  
      */
 333  
     public String getBookCharset() {
 334  0
         return ENCODING_JAVA.get(getProperty(KEY_ENCODING));
 335  
     }
 336  
 
 337  
     /* This value cannot be overridden by front-ends/jsword
 338  
      * (non-Javadoc)
 339  
      * @see org.crosswire.jsword.book.basic.AbstractBookMetaData#getKeyType()
 340  
      */
 341  
     @Override
 342  
     public KeyType getKeyType() {
 343  0
         BookType bt = getBookType();
 344  0
         if (bt == null) {
 345  0
             return null;
 346  
         }
 347  0
         return bt.getKeyType();
 348  
     }
 349  
 
 350  
     /**
 351  
      * @return the book type
 352  
      */
 353  
     public BookType getBookType() {
 354  0
         return bookType;
 355  
     }
 356  
 
 357  
     /**
 358  
      * @return the SourceFilter based upon the SourceType.
 359  
      */
 360  
     public SourceFilter getFilter() {
 361  0
         String sourcetype = getProperty(KEY_SOURCE_TYPE);
 362  0
         return SourceFilterFactory.getFilter(sourcetype);
 363  
     }
 364  
 
 365  
     /**
 366  
      * To maintain backwards compatibility, this always returns the Sword conf
 367  
      * file Get the conf file for this SwordMetaData.
 368  
      *
 369  
      * @return Returns the conf file or null if loaded from a byte buffer.
 370  
      */
 371  
     public File getConfigFile() {
 372  0
         return configFile;
 373  
     }
 374  
 
 375  
     /* Cannot be overridden by a front-end/jsword
 376  
      * (non-Javadoc)
 377  
      * @see org.crosswire.jsword.book.BookMetaData#getBookCategory()
 378  
      */
 379  
     public BookCategory getBookCategory() {
 380  0
         if (bookCat == null) {
 381  0
             bookCat = (BookCategory) getValue(KEY_CATEGORY);
 382  0
             if (bookCat == BookCategory.OTHER) {
 383  0
                 BookType bt = getBookType();
 384  0
                 if (bt == null) {
 385  0
                     return bookCat;
 386  
                 }
 387  0
                 bookCat = bt.getBookCategory();
 388  
             }
 389  
         }
 390  0
         return bookCat;
 391  
     }
 392  
 
 393  
     /* Cannot be overridden by a front-end/jsword
 394  
      * (non-Javadoc)
 395  
      * @see org.crosswire.jsword.book.basic.AbstractBookMetaData#toOSIS()
 396  
      */
 397  
     @Override
 398  
     public Document toOSIS() {
 399  0
         List<String> knownKeys = new ArrayList(configAll.getKeys());
 400  0
         OSISUtil.OSISFactory factory = OSISUtil.factory();
 401  0
         Element table = factory.createTable();
 402  0
         Element row = toRow(factory, "Initials", getInitials());
 403  0
         table.addContent(row);
 404  
         // Each key gets one row.
 405  0
         for (String key : OSIS_INFO) {
 406  0
             knownKeys.remove(key);
 407  0
             row = toRow(factory, key);
 408  0
             if (row != null) {
 409  0
                 table.addContent(row);
 410  
             }
 411  
         }
 412  
         // Output the rest in the order that they are in the conf
 413  
         // however, don't show those that should be hidden.
 414  0
         List<String> hide = Arrays.asList(HIDDEN);
 415  0
         for (String key : knownKeys) {
 416  0
             if (hide.contains(key)) {
 417  0
                 continue;
 418  
             }
 419  0
             row = toRow(factory, key);
 420  0
             if (row != null) {
 421  0
                 table.addContent(row);
 422  
             }
 423  
         }
 424  0
         return new Document(table);
 425  
     }
 426  
 
 427  
     public static void normalize(Writer out, final IniSection config, final String[] order) {
 428  0
         PrintWriter writer = null;
 429  0
         if (out instanceof PrintWriter) {
 430  0
             writer = (PrintWriter) out;
 431  
         } else {
 432  0
             writer = new PrintWriter(out);
 433  
         }
 434  0
         IniSection copy = new IniSection(config);
 435  
         // History is a special case
 436  0
         adjustHistory(copy);
 437  
 
 438  0
         List<String> knownKeys = new ArrayList(copy.getKeys());
 439  0
         writer.print("[");
 440  0
         writer.print(copy.getName());
 441  0
         writer.print("]");
 442  0
         writer.println();
 443  
 
 444  
         // Get the keys out in the specified order
 445  0
         for (String key : order) {
 446  0
             knownKeys.remove(key);
 447  0
             if (!copy.containsKey(key)) {
 448  0
                 continue;
 449  
             }
 450  0
             Collection<String> values = copy.getValues(key);
 451  0
             Iterator<String> iter = values.iterator();
 452  
             String value;
 453  0
             while (iter.hasNext()) {
 454  0
                 value = iter.next();
 455  0
                 String newKey = key;
 456  
                 // When key is History, it needs to be reversed
 457  0
                 if (KEY_HISTORY.equalsIgnoreCase(key)) {
 458  0
                     int pos = value.indexOf(' ');
 459  0
                     newKey += '_' + value.substring(0, pos);
 460  0
                     value = value.substring(pos + 1);
 461  
                 }
 462  0
                 writer.print(newKey);
 463  0
                 writer.print("=");
 464  0
                 writer.print(value.replaceAll("\n", " \\\\\n"));
 465  0
                 writer.println();
 466  0
             }
 467  
         }
 468  
 
 469  0
         Iterator<String> keys = knownKeys.iterator();
 470  0
         while (keys.hasNext()) {
 471  0
             String key = keys.next();
 472  0
             Collection<String> values = copy.getValues(key);
 473  0
             Iterator<String> iter = values.iterator();
 474  
             String value;
 475  0
             while (iter.hasNext()) {
 476  0
                 value = iter.next();
 477  0
                 writer.print(key);
 478  0
                 writer.print("=");
 479  0
                 writer.print(value.replaceAll("\n", " \\\\\n"));
 480  0
                 writer.println();
 481  
             }
 482  0
         }
 483  
 
 484  0
         writer.flush();
 485  0
     }
 486  
 
 487  
     /* (non-Javadoc)
 488  
      * @see org.crosswire.jsword.book.BookMetaData#getInitials()
 489  
      */
 490  
     public String getInitials() {
 491  0
         return configAll.getName();
 492  
     }
 493  
 
 494  
     /**
 495  
      * @return the internal name of the module, useful when re-constructing all
 496  
      *         the meta-information, after installation for example
 497  
      */
 498  
     String getInternalName() {
 499  0
         return configAll.getName();
 500  
     }
 501  
 
 502  
     /* (non-Javadoc)
 503  
      * @see org.crosswire.jsword.book.BookMetaData#getAbbreviation()
 504  
      */
 505  
     public String getAbbreviation() {
 506  0
         String abbreviation = getProperty(KEY_ABBREVIATION);
 507  0
         if (abbreviation != null && abbreviation.length() > 0) {
 508  0
             return abbreviation;
 509  
         }
 510  0
         return getInitials();
 511  
     }
 512  
 
 513  
     /* (non-Javadoc)
 514  
      * @see org.crosswire.jsword.book.BookMetaData#isLeftToRight()
 515  
      */
 516  
     public boolean isLeftToRight() {
 517  
         // This should return the dominate direction of the text, if it is BiDi,
 518  
         // then we have to guess.
 519  0
         String dir = getProperty(KEY_DIRECTION);
 520  0
         if (ConfigEntryType.DIRECTION_BIDI.equalsIgnoreCase(dir)) {
 521  
             // When BiDi, return the dominate direction based upon the Book's
 522  
             // Language not Direction
 523  0
             Language lang = getLanguage();
 524  0
             return lang.isLeftToRight();
 525  
         }
 526  
 
 527  0
         return ConfigEntryType.DIRECTION_LTOR.equalsIgnoreCase(dir);
 528  
     }
 529  
 
 530  
     /* (non-Javadoc)
 531  
      * @see org.crosswire.jsword.book.basic.AbstractBookMetaData#hasFeature(org.crosswire.jsword.book.FeatureType)
 532  
      */
 533  
     @Override
 534  
     public boolean hasFeature(FeatureType feature) {
 535  0
         String name = feature.toString();
 536  
         // Features are a positive statement.
 537  
         // If we find it mentioned anywhere, then it true
 538  0
         if (configAll.containsValue(KEY_FEATURE, name)) {
 539  0
             return true;
 540  
         }
 541  
         // Many "features" are GlobalOptionFilters, which in the Sword C++ API
 542  
         // indicate a class to use for filtering.
 543  
         // These mostly have the source type prepended to the feature
 544  0
         StringBuilder buffer = new StringBuilder(getProperty(KEY_SOURCE_TYPE));
 545  0
         buffer.append(name);
 546  0
         if (configAll.containsValue(KEY_GLOBAL_OPTION_FILTER, buffer.toString())) {
 547  0
             return true;
 548  
         }
 549  
 
 550  
         // Check for the alias prefixed by the source type
 551  0
         String alias = feature.getAlias();
 552  0
         buffer.setLength(0);
 553  0
         buffer.append(getProperty(KEY_SOURCE_TYPE));
 554  0
         buffer.append(alias);
 555  
 
 556  
         // But some do not
 557  0
         return configAll.containsValue(KEY_GLOBAL_OPTION_FILTER, name) || configAll.containsValue(KEY_GLOBAL_OPTION_FILTER, buffer.toString());
 558  
 
 559  
     }
 560  
 
 561  
     /* (non-Javadoc)
 562  
      * @see org.crosswire.jsword.book.BookMetaData#getPropertyKeys()
 563  
      */
 564  
     public Set<String> getPropertyKeys() {
 565  0
         return null;
 566  
     }
 567  
 
 568  
     /* (non-Javadoc)
 569  
      * @see org.crosswire.jsword.book.BookMetaData#getProperty(java.lang.String)
 570  
      */
 571  
     public String getProperty(String key) {
 572  0
         if (KEY_LANGUAGE.equals(key)) {
 573  0
             return getLanguage().getName();
 574  
         }
 575  0
         return configAll.get(key, DEFAULTS.get(key));
 576  
     }
 577  
 
 578  
     /* (non-Javadoc)
 579  
      * @see org.crosswire.jsword.book.basic.AbstractBookMetaData#setProperty(java.lang.String, java.lang.String)
 580  
      */
 581  
     public void setProperty(String key, String value) {
 582  0
         configAll.replace(key, value);
 583  0
     }
 584  
 
 585  
     /* (non-Javadoc)
 586  
      * @see org.crosswire.jsword.book.BookMetaData#putProperty(java.lang.String, java.lang.String, boolean)
 587  
      */
 588  
     public void putProperty(String key, String value, boolean forFrontend) {
 589  0
         MetaDataLocator mdl = forFrontend ? MetaDataLocator.FRONTEND : MetaDataLocator.JSWORD;
 590  0
         putProperty(key, value, mdl);
 591  0
     }
 592  
 
 593  
     /**
 594  
      * Allow specification of a specific SwordMetaDataLocator when saving a property.
 595  
      * 
 596  
      * @param key the entry that we are saving
 597  
      * @param value the value of the entry
 598  
      * @param metaDataLocator Place to save - front end storage, shared storage, or don't save(transient)
 599  
      */
 600  
     public void putProperty(String key, String value, MetaDataLocator metaDataLocator) {
 601  
         // Set the property for all to see
 602  0
         setProperty(key, value);
 603  
 
 604  
         // Properties are only saved for installed books.
 605  0
         if (!installed) {
 606  0
             return;
 607  
         }
 608  
 
 609  0
         File writeLocation = metaDataLocator.getWriteLocation();
 610  
         // Properties are only saved if there is a place for saving
 611  0
         if (writeLocation == null) {
 612  0
             return;
 613  
         }
 614  
         // persist property for future sessions if JSword or Front-end MetaDataLocator
 615  
         // Wait until the first write to actually create the appropriate internal storage.
 616  0
         IniSection config = null;
 617  0
         switch (metaDataLocator) {
 618  
         case FRONTEND:
 619  0
             if (this.configFrontend == null) {
 620  0
                 this.configFrontend = new IniSection(configAll.getName());
 621  
             }
 622  0
             config = this.configFrontend;
 623  0
             break;
 624  
         case JSWORD:
 625  0
             if (this.configJSword == null) {
 626  0
                 this.configJSword = new IniSection(configAll.getName());
 627  
             }
 628  0
             config = this.configJSword;
 629  0
             break;
 630  
         case TRANSIENT:
 631  
         default:
 632  
             break;
 633  
         }
 634  
 
 635  0
         if (config != null) {
 636  0
             config.replace(key, value);
 637  
             try {
 638  0
                 config.save(new File(writeLocation, bookConf), getBookCharset());
 639  0
             } catch (IOException ex) {
 640  0
                 LOGGER.error("Unable to save {}={}: conf file for [{}]; error={}", key, value, configAll.getName(), ex);
 641  0
             }
 642  
         }
 643  0
     }
 644  
 
 645  
     /**
 646  
      * Allow for partial loading of a minimum set of keys, saving time and space.
 647  
      * If partial, call reload(null) to fill it in before showing the conf contents to a user.
 648  
      * 
 649  
      * @param partial
 650  
      */
 651  
     public static void setPartialLoading(boolean partial) {
 652  
 
 653  0
         if (partial != partialLoading) {
 654  0
             if (partial) {
 655  0
                 keyKeepers = new KeyFilter(REQUIRED);
 656  
             } else {
 657  0
                 keyKeepers = null;
 658  
             }
 659  
         }
 660  0
         partialLoading = partial;
 661  0
     }
 662  
 
 663  
     /**
 664  
      * Load the conf from a file.
 665  
      *
 666  
      * @param keepers
 667  
      *            the keys to keep. When null keep all
 668  
      * @throws IOException
 669  
      */
 670  
     private void loadFile(Filter<String> keepers) throws IOException {
 671  0
         filtered = keepers != null;
 672  
 
 673  0
         configAll.clear();
 674  0
         configAll.load(configFile, ENCODING_UTF8, keepers);
 675  0
         String encoding = configAll.get(KEY_ENCODING);
 676  0
         if (!ENCODING_UTF8.equalsIgnoreCase(encoding)) {
 677  0
             configAll.clear();
 678  0
             configAll.load(configFile, ENCODING_LATIN1, keepers);
 679  
         }
 680  0
     }
 681  
 
 682  
     /**
 683  
      * Load the conf from a buffer. This is used to load conf entries from the cached mods.d.tar.gz or mods.d.zip file.
 684  
      *
 685  
      * @param buffer
 686  
      *            the buffer to load
 687  
      * @throws IOException
 688  
      */
 689  
     private void loadBuffer(byte[] buffer, Filter<String> keepers) throws IOException {
 690  0
         filtered = keepers != null;
 691  
 
 692  0
         configAll.clear();
 693  0
         configAll.load(buffer, ENCODING_UTF8, keepers);
 694  0
         String encoding = configAll.get(KEY_ENCODING);
 695  0
         if (!ENCODING_UTF8.equalsIgnoreCase(encoding)) {
 696  0
             configAll.clear();
 697  0
             configAll.load(buffer, ENCODING_LATIN1, keepers);
 698  
         }
 699  0
     }
 700  
 
 701  
     private IniSection addConfig(MetaDataLocator locator) {
 702  
         // The write location supersedes the read location
 703  0
         File conf = new File(locator.getWriteLocation(), bookConf);
 704  0
         if (!conf.exists()) {
 705  0
             conf = new File(locator.getReadLocation(), bookConf);
 706  
         }
 707  
 
 708  0
         if (conf.exists()) {
 709  
             // The additional confs have the same encoding as the SWORD conf.
 710  0
             String encoding = getProperty(KEY_ENCODING);
 711  
             try {
 712  0
                 IniSection config = new IniSection();
 713  0
                 config.load(conf, encoding);
 714  0
                 mergeConfig(config);
 715  0
                 return config;
 716  0
             } catch (IOException e) {
 717  0
                 LOGGER.error("Unable to load conf {}:{}", conf, e);
 718  
             }
 719  
         }
 720  
 
 721  0
         return null;
 722  
     }
 723  
 
 724  
     private void mergeConfig(IniSection config) {
 725  0
         for (String key : config.getKeys()) {
 726  0
             ConfigEntryType type = ConfigEntryType.fromString(key);
 727  0
             for (String value : config.getValues(key)) {
 728  0
                 if (type != null && type.mayRepeat()) {
 729  0
                     if (!configAll.containsValue(key, value)) {
 730  0
                         configAll.add(key, value);
 731  
                     }
 732  
                 } else {
 733  0
                     setProperty(key, value);
 734  
                 }
 735  
             }
 736  0
         }
 737  0
     }
 738  
 
 739  
     /**
 740  
      * Gets a particular entry value by its type
 741  
      *
 742  
      * @param key of the entry
 743  
      * @return the requested value, the default (if there is no entry) or null
 744  
      *         (if there is no default)
 745  
      */
 746  
     private Object getValue(String key) {
 747  0
         ConfigEntryType type = ConfigEntryType.fromString(key);
 748  0
         String ce = getProperty(key);
 749  0
         if (type == null) {
 750  0
             return ce;
 751  
         }
 752  
 
 753  0
         return ce == null ? null : type.convert(ce);
 754  
     }
 755  
 
 756  
     private Element toRow(OSISUtil.OSISFactory factory, String key, String value) {
 757  0
         Element nameEle = toKeyCell(factory, key);
 758  
 
 759  0
         Element valueElement = factory.createCell();
 760  0
         valueElement.addContent(value);
 761  
 
 762  
         // Each key gets one row.
 763  0
         Element rowEle = factory.createRow();
 764  0
         rowEle.addContent(nameEle);
 765  0
         rowEle.addContent(valueElement);
 766  0
         return rowEle;
 767  
     }
 768  
 
 769  
     private Element toRow(OSISUtil.OSISFactory factory, String key) {
 770  0
         int size = configAll.size(key);
 771  0
         if (size == 0) {
 772  0
             return null;
 773  
         }
 774  
 
 775  
         // See if it is a predefined type
 776  0
         ConfigEntryType type = ConfigEntryType.fromString(key);
 777  0
         Element nameEle = toKeyCell(factory, key);
 778  
 
 779  0
         Element valueElement = factory.createCell();
 780  0
         for (int j = 0; j < size; j++) {
 781  0
             if (j > 0) {
 782  0
                 valueElement.addContent(factory.createLB());
 783  
             }
 784  
 
 785  0
             String text = configAll.get(key, j);
 786  0
             if (type != null && !type.isText() && type.isAllowed(text)) {
 787  0
                 text = type.convert(text).toString();
 788  
             }
 789  0
             text = XMLUtil.escape(text);
 790  0
             if (type != null && type.allowsRTF()) {
 791  0
                 valueElement.addContent(OSISUtil.rtfToOsis(text));
 792  
             } else {
 793  0
                 valueElement.addContent(text);
 794  
             }
 795  
         }
 796  
 
 797  
         // Each key gets one row.
 798  0
         Element rowEle = factory.createRow();
 799  0
         rowEle.addContent(nameEle);
 800  0
         rowEle.addContent(valueElement);
 801  
 
 802  0
         return rowEle;
 803  
     }
 804  
 
 805  
     private Element toKeyCell(OSISUtil.OSISFactory factory, String key) {
 806  0
         Element nameEle = factory.createCell();
 807  0
         Element hiEle = factory.createHI();
 808  0
         hiEle.setAttribute(OSISUtil.OSIS_ATTR_TYPE, OSISUtil.HI_BOLD);
 809  0
         nameEle.addContent(hiEle);
 810  
         // I18N(DMS): use name to lookup translation.
 811  0
         hiEle.addContent(key);
 812  0
         return nameEle;
 813  
     }
 814  
 
 815  
     private void adjustConfig() throws BookException {
 816  0
         adjustLocation();
 817  0
         adjustLanguage();
 818  0
         adjustBookType();
 819  0
         adjustName();
 820  0
         adjustHistory(configAll);
 821  0
     }
 822  
 
 823  
     private void adjustLanguage() {
 824  0
         String lang = getProperty(KEY_LANG);
 825  0
         testLanguage(KEY_LANG, lang);
 826  
 
 827  0
         String langFrom = configAll.get(KEY_GLOSSARY_FROM);
 828  0
         String langTo = configAll.get(KEY_GLOSSARY_TO);
 829  
 
 830  
         // If we have either langFrom or langTo, we are dealing with a glossary
 831  0
         if (langFrom != null || langTo != null) {
 832  0
             if (langFrom == null) {
 833  0
                 langFrom = lang;
 834  0
                 setProperty(KEY_GLOSSARY_FROM, langFrom);
 835  0
                 LOGGER.warn("Missing data for [{}]. Assuming {}={}", configAll.getName(), KEY_GLOSSARY_FROM, langFrom);
 836  
             }
 837  0
             testLanguage(KEY_GLOSSARY_FROM, langFrom);
 838  
 
 839  0
             if (langTo == null) {
 840  0
                 langTo = Language.DEFAULT_LANG.getGivenSpecification();
 841  0
                 setProperty(KEY_GLOSSARY_TO, langTo);
 842  0
                 LOGGER.warn("Missing data for [{}]. Assuming {}={}", configAll.getName(), KEY_GLOSSARY_TO, langTo);
 843  
             }
 844  0
             testLanguage(KEY_GLOSSARY_TO, langTo);
 845  
 
 846  
             // At least one of the two languages should match the lang entry
 847  0
             if (!langFrom.equals(lang) && !langTo.equals(lang)) {
 848  0
                 LOGGER.error("Data error in [{}]. Neither {} or {} match {}", configAll.getName(), KEY_GLOSSARY_FROM, KEY_GLOSSARY_TO, KEY_LANG);
 849  
             }
 850  
         }
 851  
 
 852  0
         setLanguage((Language) getValue(KEY_LANG));
 853  0
     }
 854  
 
 855  
     private void testLanguage(String key, String lang) {
 856  0
         Language language = new Language(lang);
 857  0
         if (!language.isValidLanguage()) {
 858  0
             LOGGER.warn("Unknown language [{}]{}={}", configAll.getName(), key, lang);
 859  
         }
 860  0
     }
 861  
 
 862  
     private void adjustBookType() {
 863  
         // The book type represents the underlying category of book.
 864  
         // Fine tune it here.
 865  0
         BookCategory focusedCategory = (BookCategory) getValue(KEY_CATEGORY);
 866  0
         questionable = focusedCategory == BookCategory.QUESTIONABLE;
 867  
 
 868  0
         String modTypeName = getProperty(KEY_MOD_DRV);
 869  0
         if (modTypeName == null) {
 870  0
             LOGGER.error("Book not supported: malformed conf file for [{}] no {} found.", configAll.getName(), KEY_MOD_DRV);
 871  0
             supported = false;
 872  0
             return;
 873  
         }
 874  
 
 875  0
         String v11n = getProperty(KEY_VERSIFICATION);
 876  0
         if (!Versifications.instance().isDefined(v11n)) {
 877  0
             LOGGER.error("Book not supported: Unknown versification for [{}]{}={}.", configAll.getName(), KEY_VERSIFICATION, v11n);
 878  0
             supported = false;
 879  0
             return;
 880  
         }
 881  
 
 882  0
         bookType = BookType.fromString(modTypeName);
 883  0
         if (bookType == null) {
 884  0
             LOGGER.error("Book not supported: malformed conf file for [{}] no book type found", configAll.getName());
 885  0
             supported = false;
 886  0
             return;
 887  
         }
 888  
 
 889  
         // The book type represents the underlying category of book.
 890  
         // Fine tune it here.
 891  0
         if (focusedCategory == BookCategory.OTHER) {
 892  0
             focusedCategory = bookType.getBookCategory();
 893  
         }
 894  
 
 895  0
         setProperty(KEY_CATEGORY, focusedCategory.getName());
 896  0
     }
 897  
 
 898  
     private void adjustName() {
 899  
         // If there is no name then use the initials name
 900  0
         if (configAll.get(KEY_DESCRIPTION) == null) {
 901  0
             LOGGER.error("Malformed conf file: missing [{}]{}=. Using {}", configAll.getName(), KEY_DESCRIPTION, configAll.getName());
 902  0
             setProperty(KEY_DESCRIPTION, configAll.getName());
 903  
         }
 904  0
     }
 905  
 
 906  
     /* This method sets the location on the sword conf file for an installed Book.
 907  
      */
 908  
     private void adjustLocation() throws BookException {
 909  
 
 910  0
         URI library = getLibrary();
 911  0
         if (library == null) {
 912  0
             return;
 913  
         }
 914  
 
 915  
         // Previously, all DATA_PATH entries end in / to indicate dirs
 916  
         // or not to indicate file prefixes.
 917  
         // This is no longer true.
 918  
         // Now we need to test the file/url to see if it exists and is a
 919  
         // directory.
 920  0
         String datapath = getProperty(KEY_DATA_PATH);
 921  0
         int lastSlash = datapath.lastIndexOf('/');
 922  
 
 923  
         // There were modules that did not have a valid DataPath.
 924  
         // This should not be necessary
 925  0
         if (lastSlash == -1) {
 926  0
             return;
 927  
         }
 928  
 
 929  
         // DataPath typically ends in a '/' to indicate a directory.
 930  
         // If so remove it.
 931  0
         boolean isDirectoryPath = false;
 932  0
         if (lastSlash == datapath.length() - 1) {
 933  0
             isDirectoryPath = true;
 934  0
             datapath = datapath.substring(0, lastSlash);
 935  
         }
 936  
 
 937  0
         URI location = NetUtil.lengthenURI(library, datapath);
 938  0
         File bookDir = new File(location.getPath());
 939  
         // For some modules, the last element of the DataPath
 940  
         // is a prefix for file names.
 941  0
         if (!bookDir.isDirectory()) {
 942  0
             if (isDirectoryPath) {
 943  
                 // TRANSLATOR: This indicates that the Book is only partially installed.
 944  0
                 throw new BookException(JSMsg.gettext("The book {0} is missing its data files", configAll.getName()));
 945  
             }
 946  
 
 947  
             // not a directory path
 948  
             // try appending .dat on the end to see if we have a file, if not,
 949  
             // then
 950  0
             if (!new File(location.getPath() + ".dat").exists()) {
 951  
                 // TRANSLATOR: This indicates that the Book is only partially
 952  
                 // installed.
 953  0
                 throw new BookException(JSMsg.gettext("The book {0} is missing its data files", configAll.getName()));
 954  
             }
 955  
 
 956  
             // then we have a module that has a prefix
 957  
             // Shorten it by one segment and test again.
 958  0
             lastSlash = datapath.lastIndexOf('/');
 959  0
             datapath = datapath.substring(0, lastSlash);
 960  0
             location = NetUtil.lengthenURI(library, datapath);
 961  
         }
 962  
 
 963  0
         setLocation(location);
 964  0
     }
 965  
 
 966  
     // History is a special case. It is of the form History_x.x
 967  
     // The ConfigEntryType is History without the _x.x.
 968  
     // We want to put x.x at the beginning of the string
 969  
     private static void adjustHistory(IniSection config) {
 970  
         // Iterate over a copy of the keys so that we don't get
 971  
         // a concurrent modification exception when we remove matching keys
 972  
         // and when we add new keys
 973  0
         List<String> keys = new ArrayList(config.getKeys());
 974  0
         for (String key : keys) {
 975  0
             String value = config.get(key);
 976  0
             ConfigEntryType type = ConfigEntryType.fromString(key);
 977  0
             if (ConfigEntryType.HISTORY.equals(type)) {
 978  0
                 config.remove(key);
 979  0
                 int pos = key.indexOf('_');
 980  0
                 value = key.substring(pos + 1) + ' ' + value;
 981  0
                 config.add(KEY_HISTORY, value);
 982  
             }
 983  0
         }
 984  0
     }
 985  
 
 986  
     public static void report(final IniSection config) {
 987  0
         StringBuilder buf = new StringBuilder(config.report());
 988  0
         for (String key : config.getKeys()) {
 989  0
             ConfigEntryType type = ConfigEntryType.fromString(key);
 990  
 
 991  0
             if (type == null) {
 992  0
                 if (key.contains("_")) {
 993  0
                     String baseKey = key.substring(0, key.indexOf('_'));
 994  0
                     type = ConfigEntryType.fromString(baseKey);
 995  
                 }
 996  
             }
 997  
 
 998  
 
 999  0
             int count = config.size(key);
 1000  0
             for (int i = 0; i < count; i++) {
 1001  0
                 String value = config.get(key, i);
 1002  
 
 1003  
                 // If it is still unknown, report and skip
 1004  0
                 if (type == null) {
 1005  0
                     buf.append("Unknown entry: ").append(key).append(" = ").append(value).append('\n');
 1006  0
                     continue;
 1007  
                 }
 1008  
 
 1009  
                 // Only CIPHER_KEYS that are empty are not ignored
 1010  0
                 if (value.length() == 0 && type != ConfigEntryType.CIPHER_KEY) {
 1011  0
                     buf.append("Unexpected empty entry: ").append(key).append(" = ").append(value).append('\n');
 1012  0
                     continue;
 1013  
                 }
 1014  
 
 1015  
                 // Filter known types of entries
 1016  0
                 value = type.filter(value);
 1017  
 
 1018  
                 // Report on fields that shouldn't have RTF but do
 1019  0
                 if (!type.allowsRTF() && RTF_PATTERN.matcher(value).find()) {
 1020  0
                     buf.append("Unexpected RTF: ").append(key).append(" = ").append(value).append('\n');
 1021  
                 }
 1022  
 
 1023  0
                 if (!type.allowsHTML() && HTML_PATTERN.matcher(value).find()) {
 1024  0
                     buf.append("Unexpected HTML: ").append(key).append(" = ").append(value).append('\n');
 1025  
                 }
 1026  
 
 1027  0
                 if (!type.isAllowed(value)) {
 1028  0
                     buf.append("Unknown config value: ").append(key).append(" = ").append(value).append('\n');
 1029  
                 }
 1030  
 
 1031  0
                 if (count > 1 && !type.mayRepeat()) {
 1032  0
                     buf.append("Unexpected repeated config key: ").append(key).append(" = ").append(value).append('\n');
 1033  
                 }
 1034  
             }
 1035  0
         }
 1036  0
         if (buf.length() > 0) {
 1037  0
             LOGGER.info("Conf report for [{}]\n{}", config.getName(), buf.toString());
 1038  
         }
 1039  0
     }
 1040  
 
 1041  
     /**
 1042  
      * Indicates whether the Book is installed or not.
 1043  
      */
 1044  
     private boolean installed;
 1045  
 
 1046  
     /**
 1047  
      * When true this BookMetaData is filtered and only partially loaded.
 1048  
      * Reloading without a filter will change this to false.
 1049  
      */
 1050  
     private boolean filtered;
 1051  
 
 1052  
     /**
 1053  
      * The name of the conf file, such as kjv.conf.
 1054  
      */
 1055  
     private String bookConf;
 1056  
 
 1057  
     /**
 1058  
      * The configAll IniSection holds the merged view of the SWORD config,
 1059  
      * configJSword, and configFrontend.
 1060  
      */
 1061  
     private IniSection configAll;
 1062  
 
 1063  
     /**
 1064  
      * configJSword holds shared configuration for all front-ends.
 1065  
      */
 1066  
     private IniSection configJSword;
 1067  
 
 1068  
     /**
 1069  
      * configFrontend contains the configuration for the current front-end.
 1070  
      */
 1071  
     private IniSection configFrontend;
 1072  
 
 1073  
     /**
 1074  
      * True if this book's config type can be used by JSword.
 1075  
      */
 1076  
     private boolean supported;
 1077  
 
 1078  
     /**
 1079  
      * The BookCategory for this Book
 1080  
      */
 1081  
     private BookCategory bookCat;
 1082  
 
 1083  
     /**
 1084  
      * The BookType for this Book
 1085  
      */
 1086  
     private BookType bookType;
 1087  
 
 1088  
     /**
 1089  
      * True if this book is considered questionable.
 1090  
      */
 1091  
     private boolean questionable;
 1092  
 
 1093  
     /**
 1094  
      * If the module's config is tied to a file remember it so that it can be
 1095  
      * updated.
 1096  
      */
 1097  
     private File configFile;
 1098  
 
 1099  
     /**
 1100  
      * These are the elements that JSword uses for control when present in a conf.
 1101  
      */
 1102  0
     private static final String[] REQUIRED = {
 1103  
             KEY_ABBREVIATION,
 1104  
             KEY_DESCRIPTION,
 1105  
             KEY_LANG,
 1106  
             KEY_CATEGORY,
 1107  
             KEY_VERSION,
 1108  
             KEY_FEATURE,
 1109  
             KEY_GLOBAL_OPTION_FILTER,
 1110  
             KEY_SIGLUM1,
 1111  
             KEY_SIGLUM2,
 1112  
             KEY_SIGLUM3,
 1113  
             KEY_SIGLUM4,
 1114  
             KEY_SIGLUM5,
 1115  
             KEY_FONT,
 1116  
             KEY_DATA_PATH,
 1117  
             KEY_MOD_DRV,
 1118  
             KEY_SOURCE_TYPE,
 1119  
             KEY_BLOCK_TYPE,
 1120  
             KEY_BLOCK_COUNT,
 1121  
             KEY_COMPRESS_TYPE,
 1122  
             KEY_ENCODING,
 1123  
             KEY_DIRECTION,
 1124  
             KEY_KEY_TYPE,
 1125  
             KEY_DISPLAY_LEVEL,
 1126  
             KEY_VERSIFICATION,
 1127  
             KEY_CASE_SENSITIVE_KEYS,
 1128  
             KEY_LOCAL_STRIP_FILTER,
 1129  
             KEY_PREFERRED_CSS_XHTML,
 1130  
             KEY_STRONGS_PADDING,
 1131  
             KEY_SEARCH_OPTION,
 1132  
             KEY_INSTALL_SIZE,
 1133  
             KEY_SCOPE,
 1134  
             KEY_BOOKLIST,
 1135  
             KEY_CIPHER_KEY
 1136  
     };
 1137  
 
 1138  
     /**
 1139  
      * KeyFilter returns true for keys that should always be present.
 1140  
      * A partially loaded SwordBookMetaData will satisfy this filter.
 1141  
      */
 1142  0
     private static final class KeyFilter implements Filter<String> {
 1143  
         /**
 1144  
          * Create a KeyFilter for the expected keys.
 1145  
          * @param keepers the list of keys that should be retained
 1146  
          */
 1147  0
         KeyFilter(String[] keepers) {
 1148  0
             this.keepers = new HashSet();
 1149  
 
 1150  
             // Load up the keepers set
 1151  0
             for (String key : keepers) {
 1152  0
                 this.keepers.add(key);
 1153  
             }
 1154  0
         }
 1155  
         public boolean test(String key) {
 1156  0
             return keepers.contains(key);
 1157  
         }
 1158  
         private Set keepers;
 1159  
     }
 1160  
 
 1161  
     private static boolean partialLoading;
 1162  
 
 1163  
     private static Filter keyKeepers;
 1164  
 
 1165  0
     private static final String[] OSIS_INFO = {
 1166  
             KEY_ABBREVIATION,
 1167  
             KEY_DESCRIPTION,
 1168  
             KEY_LANG,
 1169  
             KEY_CATEGORY,
 1170  
             KEY_LCSH,
 1171  
             KEY_SWORD_VERSION_DATE,
 1172  
             KEY_VERSION,
 1173  
             KEY_HISTORY,
 1174  
             KEY_OBSOLETES,
 1175  
             KEY_GLOSSARY_FROM,
 1176  
             KEY_GLOSSARY_TO,
 1177  
             KEY_ABOUT,
 1178  
             KEY_SHORT_PROMO,
 1179  
             KEY_DISTRIBUTION_LICENSE,
 1180  
             KEY_DISTRIBUTION_NOTES,
 1181  
             KEY_DISTRIBUTION_SOURCE,
 1182  
             KEY_SHORT_COPYRIGHT,
 1183  
             KEY_COPYRIGHT,
 1184  
             KEY_COPYRIGHT_DATE,
 1185  
             KEY_COPYRIGHT_HOLDER,
 1186  
             KEY_COPYRIGHT_CONTACT_NAME,
 1187  
             KEY_COPYRIGHT_CONTACT_ADDRESS,
 1188  
             KEY_COPYRIGHT_CONTACT_EMAIL,
 1189  
             KEY_COPYRIGHT_CONTACT_NOTES,
 1190  
             KEY_COPYRIGHT_NOTES,
 1191  
             KEY_TEXT_SOURCE,
 1192  
             KEY_FEATURE,
 1193  
             KEY_GLOBAL_OPTION_FILTER,
 1194  
             KEY_SIGLUM1,
 1195  
             KEY_SIGLUM2,
 1196  
             KEY_SIGLUM3,
 1197  
             KEY_SIGLUM4,
 1198  
             KEY_SIGLUM5,
 1199  
             KEY_FONT,
 1200  
             KEY_DATA_PATH,
 1201  
             KEY_MOD_DRV,
 1202  
             KEY_SOURCE_TYPE,
 1203  
             KEY_BLOCK_TYPE,
 1204  
             KEY_BLOCK_COUNT,
 1205  
             KEY_COMPRESS_TYPE,
 1206  
             KEY_ENCODING,
 1207  
             KEY_MINIMUM_VERSION,
 1208  
             KEY_OSIS_VERSION,
 1209  
             KEY_OSIS_Q_TO_TICK,
 1210  
             KEY_DIRECTION,
 1211  
             KEY_KEY_TYPE,
 1212  
             KEY_DISPLAY_LEVEL,
 1213  
             KEY_VERSIFICATION,
 1214  
             KEY_CASE_SENSITIVE_KEYS,
 1215  
             KEY_LOCAL_STRIP_FILTER,
 1216  
             KEY_PREFERRED_CSS_XHTML,
 1217  
             KEY_STRONGS_PADDING,
 1218  
             KEY_SEARCH_OPTION,
 1219  
             KEY_INSTALL_SIZE,
 1220  
             KEY_SCOPE,
 1221  
             KEY_BOOKLIST
 1222  
     };
 1223  
 
 1224  0
     private static final String[] HIDDEN = {
 1225  
         KEY_CIPHER_KEY,
 1226  
         KEY_LANGUAGE
 1227  
     };
 1228  
 
 1229  0
     private static final Pattern RTF_PATTERN = Pattern.compile("\\\\pard|\\\\pa[er]|\\\\qc|\\\\[bi]|\\\\u-?[0-9]{4,6}+");
 1230  0
     private static final Pattern HTML_PATTERN = Pattern.compile("(<[a-zA-Z]|[a-zA-Z]>)");
 1231  
 
 1232  
     /**
 1233  
      * Sword only recognizes two encodings for its modules: UTF-8 and Latin-1
 1234  
      * Sword uses MS Windows cp1252 for Latin 1 not the standard.
 1235  
      * Arrgh! The encoding strings need to be converted to Java charsets
 1236  
      */
 1237  
     private static final String ENCODING_UTF8 = "UTF-8";
 1238  
     private static final String ENCODING_LATIN1 = "WINDOWS-1252";
 1239  0
     private static final PropertyMap ENCODING_JAVA = new PropertyMap();
 1240  
     static {
 1241  0
         ENCODING_JAVA.put("Latin-1", ENCODING_LATIN1);
 1242  0
         ENCODING_JAVA.put("UTF-8", ENCODING_UTF8);
 1243  
     }
 1244  
 
 1245  
     /**
 1246  
      * The log stream
 1247  
      */
 1248  0
     private static final Logger LOGGER = LoggerFactory.getLogger(SwordBookMetaData.class);
 1249  
 
 1250  
 }