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         Map<String, String> tempMap = new HashMap<String, String>();
164         tempMap.put(KEY_COMPRESS_TYPE, "LZSS");
165         tempMap.put(KEY_BLOCK_TYPE, "CHAPTER");
166         tempMap.put(KEY_BLOCK_COUNT, "200");
167         tempMap.put(KEY_KEY_TYPE, "TreeKey");
168         tempMap.put(KEY_VERSIFICATION, "KJV");
169         tempMap.put(KEY_DIRECTION, "LtoR");
170         tempMap.put(KEY_SOURCE_TYPE, "Plaintext");
171         tempMap.put(KEY_ENCODING, "Latin-1");
172         tempMap.put(KEY_DISPLAY_LEVEL, "1");
173         tempMap.put(KEY_OSIS_Q_TO_TICK, "true");
174         tempMap.put(KEY_VERSION, "1.0");
175         tempMap.put(KEY_MINIMUM_VERSION, "1.5.1a");
176         tempMap.put(KEY_CATEGORY, "Other");
177         tempMap.put(KEY_LANG, "en");
178         tempMap.put(KEY_DISTRIBUTION_LICENSE, "Public Domain");
179         tempMap.put(KEY_CASE_SENSITIVE_KEYS, "false");
180         tempMap.put(KEY_STRONGS_PADDING, "true");
181         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     public SwordBookMetaData(File file, URI bookRootPath) throws IOException, BookException {
196         this.installed = true;
197         this.configFile = file;
198         this.bookConf = file.getName(); // something like kjv.conf
199         setLibrary(bookRootPath);
200 
201         this.configAll = new IniSection();
202         this.filtered = true; // Force it to run.
203         reload(keyKeepers);
204     }
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     public SwordBookMetaData(byte[] buffer, String bookConf) throws IOException, BookException {
215         this.installed = false;
216         this.bookConf = bookConf; // something like .../mods.d.zip!mods.d/kjv.conf
217         this.supported = true;
218         this.configAll = new IniSection();
219         this.filtered = true; // Force it to run.
220         loadBuffer(buffer, keyKeepers);
221         adjustConfig();
222         report(configAll);
223     }
224 
225     /* (non-Javadoc)
226      * @see org.crosswire.jsword.book.basic.AbstractBookMetaData#reload()
227      */
228     @Override
229     public void reload() throws BookException {
230         reload(null);
231     }
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         if (!filtered && keepers == null) {
238             return;
239         }
240         this.supported = true;
241         if (configJSword != null) {
242             configJSword.clear();
243         }
244         if (configFrontend != null) {
245             configFrontend.clear();
246         }
247         try {
248             if (installed) {
249                 loadFile(keepers);
250             } else {
251                 byte[] buffer = IOUtil.getZipEntry(bookConf);
252                 loadBuffer(buffer, keepers);
253             }
254             adjustConfig();
255             report(configAll);
256 
257             this.configJSword = addConfig(MetaDataLocator.JSWORD);
258             this.configFrontend = addConfig(MetaDataLocator.FRONTEND);
259         } catch (IOException ex) {
260             throw new BookException("unable to load conf", ex);
261         }
262     }
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         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         return supported;
280     }
281 
282     /* (non-Javadoc)
283     * @see org.crosswire.jsword.book.basic.AbstractBookMetaData#isEnciphered()
284     */
285     @Override
286     public boolean isEnciphered() {
287         String cipher = getProperty(KEY_CIPHER_KEY);
288         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         String cipher = getProperty(KEY_CIPHER_KEY);
298         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         putProperty(KEY_CIPHER_KEY, unlockKey, false);
308         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         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         return getProperty(KEY_DESCRIPTION);
328     }
329 
330     /* (non-Javadoc)
331      * @see org.crosswire.jsword.book.BookMetaData#getBookCharset()
332      */
333     public String getBookCharset() {
334         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         BookType bt = getBookType();
344         if (bt == null) {
345             return null;
346         }
347         return bt.getKeyType();
348     }
349 
350     /**
351      * @return the book type
352      */
353     public BookType getBookType() {
354         return bookType;
355     }
356 
357     /**
358      * @return the SourceFilter based upon the SourceType.
359      */
360     public SourceFilter getFilter() {
361         String sourcetype = getProperty(KEY_SOURCE_TYPE);
362         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         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         if (bookCat == null) {
381             bookCat = (BookCategory) getValue(KEY_CATEGORY);
382             if (bookCat == BookCategory.OTHER) {
383                 BookType bt = getBookType();
384                 if (bt == null) {
385                     return bookCat;
386                 }
387                 bookCat = bt.getBookCategory();
388             }
389         }
390         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         List<String> knownKeys = new ArrayList(configAll.getKeys());
400         OSISUtil.OSISFactory factory = OSISUtil.factory();
401         Element table = factory.createTable();
402         Element row = toRow(factory, "Initials", getInitials());
403         table.addContent(row);
404         // Each key gets one row.
405         for (String key : OSIS_INFO) {
406             knownKeys.remove(key);
407             row = toRow(factory, key);
408             if (row != null) {
409                 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         List<String> hide = Arrays.asList(HIDDEN);
415         for (String key : knownKeys) {
416             if (hide.contains(key)) {
417                 continue;
418             }
419             row = toRow(factory, key);
420             if (row != null) {
421                 table.addContent(row);
422             }
423         }
424         return new Document(table);
425     }
426 
427     public static void normalize(Writer out, final IniSection config, final String[] order) {
428         PrintWriter writer = null;
429         if (out instanceof PrintWriter) {
430             writer = (PrintWriter) out;
431         } else {
432             writer = new PrintWriter(out);
433         }
434         IniSection copy = new IniSection(config);
435         // History is a special case
436         adjustHistory(copy);
437 
438         List<String> knownKeys = new ArrayList(copy.getKeys());
439         writer.print("[");
440         writer.print(copy.getName());
441         writer.print("]");
442         writer.println();
443 
444         // Get the keys out in the specified order
445         for (String key : order) {
446             knownKeys.remove(key);
447             if (!copy.containsKey(key)) {
448                 continue;
449             }
450             Collection<String> values = copy.getValues(key);
451             Iterator<String> iter = values.iterator();
452             String value;
453             while (iter.hasNext()) {
454                 value = iter.next();
455                 String newKey = key;
456                 // When key is History, it needs to be reversed
457                 if (KEY_HISTORY.equalsIgnoreCase(key)) {
458                     int pos = value.indexOf(' ');
459                     newKey += '_' + value.substring(0, pos);
460                     value = value.substring(pos + 1);
461                 }
462                 writer.print(newKey);
463                 writer.print("=");
464                 writer.print(value.replaceAll("\n", " \\\\\n"));
465                 writer.println();
466             }
467         }
468 
469         Iterator<String> keys = knownKeys.iterator();
470         while (keys.hasNext()) {
471             String key = keys.next();
472             Collection<String> values = copy.getValues(key);
473             Iterator<String> iter = values.iterator();
474             String value;
475             while (iter.hasNext()) {
476                 value = iter.next();
477                 writer.print(key);
478                 writer.print("=");
479                 writer.print(value.replaceAll("\n", " \\\\\n"));
480                 writer.println();
481             }
482         }
483 
484         writer.flush();
485     }
486 
487     /* (non-Javadoc)
488      * @see org.crosswire.jsword.book.BookMetaData#getInitials()
489      */
490     public String getInitials() {
491         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         return configAll.getName();
500     }
501 
502     /* (non-Javadoc)
503      * @see org.crosswire.jsword.book.BookMetaData#getAbbreviation()
504      */
505     public String getAbbreviation() {
506         String abbreviation = getProperty(KEY_ABBREVIATION);
507         if (abbreviation != null && abbreviation.length() > 0) {
508             return abbreviation;
509         }
510         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         String dir = getProperty(KEY_DIRECTION);
520         if (ConfigEntryType.DIRECTION_BIDI.equalsIgnoreCase(dir)) {
521             // When BiDi, return the dominate direction based upon the Book's
522             // Language not Direction
523             Language lang = getLanguage();
524             return lang.isLeftToRight();
525         }
526 
527         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         String name = feature.toString();
536         // Features are a positive statement.
537         // If we find it mentioned anywhere, then it true
538         if (configAll.containsValue(KEY_FEATURE, name)) {
539             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         StringBuilder buffer = new StringBuilder(getProperty(KEY_SOURCE_TYPE));
545         buffer.append(name);
546         if (configAll.containsValue(KEY_GLOBAL_OPTION_FILTER, buffer.toString())) {
547             return true;
548         }
549 
550         // Check for the alias prefixed by the source type
551         String alias = feature.getAlias();
552         buffer.setLength(0);
553         buffer.append(getProperty(KEY_SOURCE_TYPE));
554         buffer.append(alias);
555 
556         // But some do not
557         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         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         if (KEY_LANGUAGE.equals(key)) {
573             return getLanguage().getName();
574         }
575         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         configAll.replace(key, value);
583     }
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         MetaDataLocator mdl = forFrontend ? MetaDataLocator.FRONTEND : MetaDataLocator.JSWORD;
590         putProperty(key, value, mdl);
591     }
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         setProperty(key, value);
603 
604         // Properties are only saved for installed books.
605         if (!installed) {
606             return;
607         }
608 
609         File writeLocation = metaDataLocator.getWriteLocation();
610         // Properties are only saved if there is a place for saving
611         if (writeLocation == null) {
612             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         IniSection config = null;
617         switch (metaDataLocator) {
618         case FRONTEND:
619             if (this.configFrontend == null) {
620                 this.configFrontend = new IniSection(configAll.getName());
621             }
622             config = this.configFrontend;
623             break;
624         case JSWORD:
625             if (this.configJSword == null) {
626                 this.configJSword = new IniSection(configAll.getName());
627             }
628             config = this.configJSword;
629             break;
630         case TRANSIENT:
631         default:
632             break;
633         }
634 
635         if (config != null) {
636             config.replace(key, value);
637             try {
638                 config.save(new File(writeLocation, bookConf), getBookCharset());
639             } catch (IOException ex) {
640                 LOGGER.error("Unable to save {}={}: conf file for [{}]; error={}", key, value, configAll.getName(), ex);
641             }
642         }
643     }
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         if (partial != partialLoading) {
654             if (partial) {
655                 keyKeepers = new KeyFilter(REQUIRED);
656             } else {
657                 keyKeepers = null;
658             }
659         }
660         partialLoading = partial;
661     }
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         filtered = keepers != null;
672 
673         configAll.clear();
674         configAll.load(configFile, ENCODING_UTF8, keepers);
675         String encoding = configAll.get(KEY_ENCODING);
676         if (!ENCODING_UTF8.equalsIgnoreCase(encoding)) {
677             configAll.clear();
678             configAll.load(configFile, ENCODING_LATIN1, keepers);
679         }
680     }
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         filtered = keepers != null;
691 
692         configAll.clear();
693         configAll.load(buffer, ENCODING_UTF8, keepers);
694         String encoding = configAll.get(KEY_ENCODING);
695         if (!ENCODING_UTF8.equalsIgnoreCase(encoding)) {
696             configAll.clear();
697             configAll.load(buffer, ENCODING_LATIN1, keepers);
698         }
699     }
700 
701     private IniSection addConfig(MetaDataLocator locator) {
702         // The write location supersedes the read location
703         File conf = new File(locator.getWriteLocation(), bookConf);
704         if (!conf.exists()) {
705             conf = new File(locator.getReadLocation(), bookConf);
706         }
707 
708         if (conf.exists()) {
709             // The additional confs have the same encoding as the SWORD conf.
710             String encoding = getProperty(KEY_ENCODING);
711             try {
712                 IniSection config = new IniSection();
713                 config.load(conf, encoding);
714                 mergeConfig(config);
715                 return config;
716             } catch (IOException e) {
717                 LOGGER.error("Unable to load conf {}:{}", conf, e);
718             }
719         }
720 
721         return null;
722     }
723 
724     private void mergeConfig(IniSection config) {
725         for (String key : config.getKeys()) {
726             ConfigEntryType type = ConfigEntryType.fromString(key);
727             for (String value : config.getValues(key)) {
728                 if (type != null && type.mayRepeat()) {
729                     if (!configAll.containsValue(key, value)) {
730                         configAll.add(key, value);
731                     }
732                 } else {
733                     setProperty(key, value);
734                 }
735             }
736         }
737     }
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         ConfigEntryType type = ConfigEntryType.fromString(key);
748         String ce = getProperty(key);
749         if (type == null) {
750             return ce;
751         }
752 
753         return ce == null ? null : type.convert(ce);
754     }
755 
756     private Element toRow(OSISUtil.OSISFactory factory, String key, String value) {
757         Element nameEle = toKeyCell(factory, key);
758 
759         Element valueElement = factory.createCell();
760         valueElement.addContent(value);
761 
762         // Each key gets one row.
763         Element rowEle = factory.createRow();
764         rowEle.addContent(nameEle);
765         rowEle.addContent(valueElement);
766         return rowEle;
767     }
768 
769     private Element toRow(OSISUtil.OSISFactory factory, String key) {
770         int size = configAll.size(key);
771         if (size == 0) {
772             return null;
773         }
774 
775         // See if it is a predefined type
776         ConfigEntryType type = ConfigEntryType.fromString(key);
777         Element nameEle = toKeyCell(factory, key);
778 
779         Element valueElement = factory.createCell();
780         for (int j = 0; j < size; j++) {
781             if (j > 0) {
782                 valueElement.addContent(factory.createLB());
783             }
784 
785             String text = configAll.get(key, j);
786             if (type != null && !type.isText() && type.isAllowed(text)) {
787                 text = type.convert(text).toString();
788             }
789             text = XMLUtil.escape(text);
790             if (type != null && type.allowsRTF()) {
791                 valueElement.addContent(OSISUtil.rtfToOsis(text));
792             } else {
793                 valueElement.addContent(text);
794             }
795         }
796 
797         // Each key gets one row.
798         Element rowEle = factory.createRow();
799         rowEle.addContent(nameEle);
800         rowEle.addContent(valueElement);
801 
802         return rowEle;
803     }
804 
805     private Element toKeyCell(OSISUtil.OSISFactory factory, String key) {
806         Element nameEle = factory.createCell();
807         Element hiEle = factory.createHI();
808         hiEle.setAttribute(OSISUtil.OSIS_ATTR_TYPE, OSISUtil.HI_BOLD);
809         nameEle.addContent(hiEle);
810         // I18N(DMS): use name to lookup translation.
811         hiEle.addContent(key);
812         return nameEle;
813     }
814 
815     private void adjustConfig() throws BookException {
816         adjustLocation();
817         adjustLanguage();
818         adjustBookType();
819         adjustName();
820         adjustHistory(configAll);
821     }
822 
823     private void adjustLanguage() {
824         String lang = getProperty(KEY_LANG);
825         testLanguage(KEY_LANG, lang);
826 
827         String langFrom = configAll.get(KEY_GLOSSARY_FROM);
828         String langTo = configAll.get(KEY_GLOSSARY_TO);
829 
830         // If we have either langFrom or langTo, we are dealing with a glossary
831         if (langFrom != null || langTo != null) {
832             if (langFrom == null) {
833                 langFrom = lang;
834                 setProperty(KEY_GLOSSARY_FROM, langFrom);
835                 LOGGER.warn("Missing data for [{}]. Assuming {}={}", configAll.getName(), KEY_GLOSSARY_FROM, langFrom);
836             }
837             testLanguage(KEY_GLOSSARY_FROM, langFrom);
838 
839             if (langTo == null) {
840                 langTo = Language.DEFAULT_LANG.getGivenSpecification();
841                 setProperty(KEY_GLOSSARY_TO, langTo);
842                 LOGGER.warn("Missing data for [{}]. Assuming {}={}", configAll.getName(), KEY_GLOSSARY_TO, langTo);
843             }
844             testLanguage(KEY_GLOSSARY_TO, langTo);
845 
846             // At least one of the two languages should match the lang entry
847             if (!langFrom.equals(lang) && !langTo.equals(lang)) {
848                 LOGGER.error("Data error in [{}]. Neither {} or {} match {}", configAll.getName(), KEY_GLOSSARY_FROM, KEY_GLOSSARY_TO, KEY_LANG);
849             }
850         }
851 
852         setLanguage((Language) getValue(KEY_LANG));
853     }
854 
855     private void testLanguage(String key, String lang) {
856         Language language = new Language(lang);
857         if (!language.isValidLanguage()) {
858             LOGGER.warn("Unknown language [{}]{}={}", configAll.getName(), key, lang);
859         }
860     }
861 
862     private void adjustBookType() {
863         // The book type represents the underlying category of book.
864         // Fine tune it here.
865         BookCategory focusedCategory = (BookCategory) getValue(KEY_CATEGORY);
866         questionable = focusedCategory == BookCategory.QUESTIONABLE;
867 
868         String modTypeName = getProperty(KEY_MOD_DRV);
869         if (modTypeName == null) {
870             LOGGER.error("Book not supported: malformed conf file for [{}] no {} found.", configAll.getName(), KEY_MOD_DRV);
871             supported = false;
872             return;
873         }
874 
875         String v11n = getProperty(KEY_VERSIFICATION);
876         if (!Versifications.instance().isDefined(v11n)) {
877             LOGGER.error("Book not supported: Unknown versification for [{}]{}={}.", configAll.getName(), KEY_VERSIFICATION, v11n);
878             supported = false;
879             return;
880         }
881 
882         bookType = BookType.fromString(modTypeName);
883         if (bookType == null) {
884             LOGGER.error("Book not supported: malformed conf file for [{}] no book type found", configAll.getName());
885             supported = false;
886             return;
887         }
888 
889         // The book type represents the underlying category of book.
890         // Fine tune it here.
891         if (focusedCategory == BookCategory.OTHER) {
892             focusedCategory = bookType.getBookCategory();
893         }
894 
895         setProperty(KEY_CATEGORY, focusedCategory.getName());
896     }
897 
898     private void adjustName() {
899         // If there is no name then use the initials name
900         if (configAll.get(KEY_DESCRIPTION) == null) {
901             LOGGER.error("Malformed conf file: missing [{}]{}=. Using {}", configAll.getName(), KEY_DESCRIPTION, configAll.getName());
902             setProperty(KEY_DESCRIPTION, configAll.getName());
903         }
904     }
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         URI library = getLibrary();
911         if (library == null) {
912             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         String datapath = getProperty(KEY_DATA_PATH);
921         int lastSlash = datapath.lastIndexOf('/');
922 
923         // There were modules that did not have a valid DataPath.
924         // This should not be necessary
925         if (lastSlash == -1) {
926             return;
927         }
928 
929         // DataPath typically ends in a '/' to indicate a directory.
930         // If so remove it.
931         boolean isDirectoryPath = false;
932         if (lastSlash == datapath.length() - 1) {
933             isDirectoryPath = true;
934             datapath = datapath.substring(0, lastSlash);
935         }
936 
937         URI location = NetUtil.lengthenURI(library, datapath);
938         File bookDir = new File(location.getPath());
939         // For some modules, the last element of the DataPath
940         // is a prefix for file names.
941         if (!bookDir.isDirectory()) {
942             if (isDirectoryPath) {
943                 // TRANSLATOR: This indicates that the Book is only partially installed.
944                 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             if (!new File(location.getPath() + ".dat").exists()) {
951                 // TRANSLATOR: This indicates that the Book is only partially
952                 // installed.
953                 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             lastSlash = datapath.lastIndexOf('/');
959             datapath = datapath.substring(0, lastSlash);
960             location = NetUtil.lengthenURI(library, datapath);
961         }
962 
963         setLocation(location);
964     }
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         List<String> keys = new ArrayList(config.getKeys());
974         for (String key : keys) {
975             String value = config.get(key);
976             ConfigEntryType type = ConfigEntryType.fromString(key);
977             if (ConfigEntryType.HISTORY.equals(type)) {
978                 config.remove(key);
979                 int pos = key.indexOf('_');
980                 value = key.substring(pos + 1) + ' ' + value;
981                 config.add(KEY_HISTORY, value);
982             }
983         }
984     }
985 
986     public static void report(final IniSection config) {
987         StringBuilder buf = new StringBuilder(config.report());
988         for (String key : config.getKeys()) {
989             ConfigEntryType type = ConfigEntryType.fromString(key);
990 
991             if (type == null) {
992                 if (key.contains("_")) {
993                     String baseKey = key.substring(0, key.indexOf('_'));
994                     type = ConfigEntryType.fromString(baseKey);
995                 }
996             }
997 
998 
999             int count = config.size(key);
1000            for (int i = 0; i < count; i++) {
1001                String value = config.get(key, i);
1002
1003                // If it is still unknown, report and skip
1004                if (type == null) {
1005                    buf.append("Unknown entry: ").append(key).append(" = ").append(value).append('\n');
1006                    continue;
1007                }
1008
1009                // Only CIPHER_KEYS that are empty are not ignored
1010                if (value.length() == 0 && type != ConfigEntryType.CIPHER_KEY) {
1011                    buf.append("Unexpected empty entry: ").append(key).append(" = ").append(value).append('\n');
1012                    continue;
1013                }
1014
1015                // Filter known types of entries
1016                value = type.filter(value);
1017
1018                // Report on fields that shouldn't have RTF but do
1019                if (!type.allowsRTF() && RTF_PATTERN.matcher(value).find()) {
1020                    buf.append("Unexpected RTF: ").append(key).append(" = ").append(value).append('\n');
1021                }
1022
1023                if (!type.allowsHTML() && HTML_PATTERN.matcher(value).find()) {
1024                    buf.append("Unexpected HTML: ").append(key).append(" = ").append(value).append('\n');
1025                }
1026
1027                if (!type.isAllowed(value)) {
1028                    buf.append("Unknown config value: ").append(key).append(" = ").append(value).append('\n');
1029                }
1030
1031                if (count > 1 && !type.mayRepeat()) {
1032                    buf.append("Unexpected repeated config key: ").append(key).append(" = ").append(value).append('\n');
1033                }
1034            }
1035        }
1036        if (buf.length() > 0) {
1037            LOGGER.info("Conf report for [{}]\n{}", config.getName(), buf.toString());
1038        }
1039    }
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    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    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        KeyFilter(String[] keepers) {
1148            this.keepers = new HashSet();
1149
1150            // Load up the keepers set
1151            for (String key : keepers) {
1152                this.keepers.add(key);
1153            }
1154        }
1155        public boolean test(String key) {
1156            return keepers.contains(key);
1157        }
1158        private Set keepers;
1159    }
1160
1161    private static boolean partialLoading;
1162
1163    private static Filter keyKeepers;
1164
1165    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    private static final String[] HIDDEN = {
1225        KEY_CIPHER_KEY,
1226        KEY_LANGUAGE
1227    };
1228
1229    private static final Pattern RTF_PATTERN = Pattern.compile("\\\\pard|\\\\pa[er]|\\\\qc|\\\\[bi]|\\\\u-?[0-9]{4,6}+");
1230    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    private static final PropertyMap ENCODING_JAVA = new PropertyMap();
1240    static {
1241        ENCODING_JAVA.put("Latin-1", ENCODING_LATIN1);
1242        ENCODING_JAVA.put("UTF-8", ENCODING_UTF8);
1243    }
1244
1245    /**
1246     * The log stream
1247     */
1248    private static final Logger LOGGER = LoggerFactory.getLogger(SwordBookMetaData.class);
1249
1250}
1251