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 as published by
5    * the Free Software Foundation. This program is distributed in the hope
6    * that it will be useful, but WITHOUT ANY WARRANTY; without even the
7    * 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   * Copyright: 2005
18   *     The copyright to this program is held by it's authors.
19   *
20   * ID: $Id: ConfigEntryTable.java 2223 2012-01-26 21:28:02Z dmsmith $
21   */
22  package org.crosswire.jsword.book.sword;
23  
24  import java.io.BufferedReader;
25  import java.io.ByteArrayInputStream;
26  import java.io.File;
27  import java.io.FileInputStream;
28  import java.io.FileOutputStream;
29  import java.io.IOException;
30  import java.io.InputStreamReader;
31  import java.io.OutputStreamWriter;
32  import java.io.Writer;
33  import java.util.HashMap;
34  import java.util.Map;
35  import java.util.Set;
36  import java.util.TreeMap;
37  import java.util.regex.Matcher;
38  import java.util.regex.Pattern;
39  
40  import org.crosswire.common.util.Language;
41  import org.crosswire.common.util.Languages;
42  import org.crosswire.common.util.Logger;
43  import org.crosswire.common.util.Reporter;
44  import org.crosswire.jsword.JSMsg;
45  import org.crosswire.jsword.book.BookCategory;
46  import org.crosswire.jsword.book.OSISUtil;
47  import org.crosswire.jsword.versification.system.Versifications;
48  import org.jdom.Element;
49  
50  /**
51   * A utility class for loading the entries in a Sword book's conf file. Since
52   * the conf files are manually maintained, there can be all sorts of errors in
53   * them. This class does robust checking and reporting.
54   * 
55   * <p>
56   * Config file format. See also: <a href=
57   * "http://sword.sourceforge.net/cgi-bin/twiki/view/Swordapi/ConfFileLayout">
58   * http://sword.sourceforge.net/cgi-bin/twiki/view/Swordapi/ConfFileLayout</a>
59   * 
60   * <p>
61   * The contents of the About field are in rtf.
62   * <p>
63   * \ is used as a continuation line.
64   * 
65   * @see gnu.lgpl.License for license details.<br>
66   *      The copyright to this program is held by it's authors.
67   * @author Mark Goodwin [mark at thorubio dot org]
68   * @author Joe Walker [joe at eireneh dot com]
69   * @author Jacky Cheung
70   * @author DM Smith [dmsmith555 at yahoo dot com]
71   */
72  public final class ConfigEntryTable {
73      /**
74       * Create an empty Sword config for the named book.
75       * 
76       * @param bookName
77       *            the name of the book
78       */
79      public ConfigEntryTable(String bookName) {
80          table = new HashMap<ConfigEntryType, ConfigEntry>();
81          extra = new TreeMap<String, ConfigEntry>();
82          internal = bookName;
83          supported = true;
84      }
85  
86      /**
87       * Load the conf from a file.
88       * 
89       * @param file
90       *            the file to load
91       * @throws IOException
92       */
93      public void load(File file) throws IOException {
94          configFile = file;
95  
96          BufferedReader in = null;
97          int bufferSize = 8192;
98          try {
99              // Quiet Android from complaining about using the default BufferReader buffer size.
100             // The actual buffer size is undocumented. So this is a good idea any way.
101             in = new BufferedReader(new InputStreamReader(new FileInputStream(file), ENCODING_UTF8), bufferSize);
102             loadInitials(in);
103             loadContents(in);
104             in.close();
105             in = null;
106             if (getValue(ConfigEntryType.ENCODING).equals(ENCODING_LATIN1)) {
107                 supported = true;
108                 bookType = null;
109                 questionable = false;
110                 readahead = null;
111                 table.clear();
112                 extra.clear();
113                 in = new BufferedReader(new InputStreamReader(new FileInputStream(file), ENCODING_LATIN1), bufferSize);
114                 loadInitials(in);
115                 loadContents(in);
116                 in.close();
117                 in = null;
118             }
119             adjustDataPath();
120             adjustLanguage();
121             adjustBookType();
122             adjustName();
123             validate();
124         } finally {
125             if (in != null) {
126                 in.close();
127             }
128         }
129     }
130 
131     /**
132      * Load the conf from a buffer. This is used to load conf entries from the
133      * mods.d.tar.gz file.
134      * 
135      * @param buffer
136      *            the buffer to load
137      * @throws IOException
138      */
139     public void load(byte[] buffer) throws IOException {
140         BufferedReader in = null;
141         try {
142             // Quiet Android from complaining about using the default BufferReader buffer size.
143             // The actual buffer size is undocumented. So this is a good idea any way.
144             in = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buffer), ENCODING_UTF8), buffer.length);
145             loadInitials(in);
146             loadContents(in);
147             in.close();
148             in = null;
149             if (getValue(ConfigEntryType.ENCODING).equals(ENCODING_LATIN1)) {
150                 supported = true;
151                 bookType = null;
152                 questionable = false;
153                 readahead = null;
154                 table.clear();
155                 extra.clear();
156                 in = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buffer), ENCODING_LATIN1), buffer.length);
157                 loadInitials(in);
158                 loadContents(in);
159                 in.close();
160                 in = null;
161             }
162             adjustDataPath();
163             adjustLanguage();
164             adjustBookType();
165             adjustName();
166             validate();
167         } finally {
168             if (in != null) {
169                 in.close();
170             }
171         }
172     }
173 
174     /**
175      * Determines whether the Sword Book's conf is dubious theologically as stated in conf.
176      */
177     public boolean isQuestionable() {
178         return questionable;
179     }
180 
181     /**
182      * Determines whether the Sword Book's conf is supported by JSword.
183      */
184     public boolean isSupported() {
185         return supported;
186     }
187 
188     /**
189      * Determines whether the Sword Book is enciphered.
190      * 
191      * @return true if enciphered
192      */
193     public boolean isEnciphered() {
194         String cipher = (String) getValue(ConfigEntryType.CIPHER_KEY);
195         return cipher != null;
196     }
197 
198     /**
199      * Determines whether the Sword Book is enciphered and without a key.
200      * 
201      * @return true if enciphered
202      */
203     public boolean isLocked() {
204         String cipher = (String) getValue(ConfigEntryType.CIPHER_KEY);
205         return cipher != null && cipher.length() == 0;
206     }
207 
208     /**
209      * Unlocks a book with the given key. The key is trimmed of any leading or
210      * trailing whitespace.
211      * 
212      * @param unlockKey
213      *            the key to try
214      * @return true if the unlock key worked.
215      */
216     public boolean unlock(String unlockKey) {
217         String tmpKey = unlockKey;
218         if (tmpKey != null) {
219             tmpKey = tmpKey.trim();
220         }
221         add(ConfigEntryType.CIPHER_KEY, tmpKey);
222         if (configFile != null) {
223             try {
224                 save();
225             } catch (IOException e) {
226                 // TRANSLATOR: Common error condition: The user supplied unlock key could not be saved.
227                 Reporter.informUser(this, JSMsg.gettext("Unable to save the book's unlock key."));
228             }
229         }
230         return true;
231     }
232 
233     /**
234      * Gets the unlock key for the module.
235      * 
236      * @return the unlock key, if any, null otherwise.
237      */
238     public String getUnlockKey() {
239         return (String) getValue(ConfigEntryType.CIPHER_KEY);
240     }
241 
242     /**
243      * Returns an Enumeration of all the known keys found in the config file.
244      */
245     public Set<ConfigEntryType> getKeys() {
246         return table.keySet();
247     }
248 
249     /**
250      * Returns an Enumeration of all the unknown keys found in the config file.
251      */
252     public Set<String> getExtraKeys() {
253         return extra.keySet();
254     }
255 
256     /**
257      * Returns an Enumeration of all the keys found in the config file.
258      */
259     public BookType getBookType() {
260         return bookType;
261     }
262 
263     /**
264      * Gets a particular ConfigEntry's value by its type
265      * 
266      * @param type
267      *            of the ConfigEntry
268      * @return the requested value, the default (if there is no entry) or null
269      *         (if there is no default)
270      */
271     public Object getValue(ConfigEntryType type) {
272         ConfigEntry ce = table.get(type);
273         if (ce != null) {
274             return ce.getValue();
275         }
276         return type.getDefault();
277     }
278 
279     /**
280      * Determine whether this ConfigEntryTable has the ConfigEntry and it
281      * matches the value.
282      * 
283      * @param type
284      *            The kind of ConfigEntry to look for
285      * @param search
286      *            the value to match against
287      * @return true if there is a matching ConfigEntry matching the value
288      */
289     public boolean match(ConfigEntryType type, String search) {
290         ConfigEntry ce = table.get(type);
291         return ce != null && ce.match(search);
292     }
293 
294     /**
295      * Sort the keys for a more meaningful presentation order.
296      */
297     public Element toOSIS() {
298         OSISUtil.OSISFactory factory = OSISUtil.factory();
299         Element ele = factory.createTable();
300         toOSIS(factory, ele, "BasicInfo", BASIC_INFO);
301         toOSIS(factory, ele, "LangInfo", LANG_INFO);
302         toOSIS(factory, ele, "LicenseInfo", COPYRIGHT_INFO);
303         toOSIS(factory, ele, "FeatureInfo", FEATURE_INFO);
304         toOSIS(factory, ele, "SysInfo", SYSTEM_INFO);
305         toOSIS(factory, ele, "Extra", extra);
306         return ele;
307     }
308 
309     /**
310      * Build's a SWORD conf file as a string. The result is not identical to the
311      * original, cleaning up problems in the original and re-arranging the
312      * entries into a predictable order.
313      * 
314      * @return the well-formed conf.
315      */
316     public String toConf() {
317         StringBuilder buf = new StringBuilder();
318         buf.append('[');
319         buf.append(getValue(ConfigEntryType.INITIALS));
320         buf.append("]\n");
321         toConf(buf, BASIC_INFO);
322         toConf(buf, SYSTEM_INFO);
323         toConf(buf, HIDDEN);
324         toConf(buf, FEATURE_INFO);
325         toConf(buf, LANG_INFO);
326         toConf(buf, COPYRIGHT_INFO);
327         toConf(buf, extra);
328         return buf.toString();
329     }
330 
331     public void save() throws IOException {
332         if (configFile != null) {
333             // The encoding of the conf must match the encoding of the module.
334             String encoding = ENCODING_LATIN1;
335             if (getValue(ConfigEntryType.ENCODING).equals(ENCODING_UTF8)) {
336                 encoding = ENCODING_UTF8;
337             }
338             Writer writer = null;
339             try {
340                 writer = new OutputStreamWriter(new FileOutputStream(configFile), encoding);
341                 writer.write(toConf());
342             } finally {
343                 if (writer != null) {
344                     writer.close();
345                 }
346             }
347         }
348     }
349 
350     public void save(File file) throws IOException {
351         this.configFile = file;
352         this.save();
353     }
354 
355     private void loadContents(BufferedReader in) throws IOException {
356         StringBuilder buf = new StringBuilder();
357         while (true) {
358             // Empty out the buffer
359             buf.setLength(0);
360 
361             String line = advance(in);
362             if (line == null) {
363                 break;
364             }
365 
366             // skip blank lines
367             if (line.length() == 0) {
368                 continue;
369             }
370 
371             Matcher matcher = KEY_VALUE_PATTERN.matcher(line);
372             if (!matcher.matches()) {
373                 log.warn("Expected to see '=' in " + internal + ": " + line);
374                 continue;
375             }
376 
377             String key = matcher.group(1).trim();
378             String value = matcher.group(2).trim();
379             // Only CIPHER_KEYS that are empty are not ignored
380             if (value.length() == 0 && !ConfigEntryType.CIPHER_KEY.getName().equals(key)) {
381                 log.warn("Ignoring empty entry in " + internal + ": " + line);
382                 continue;
383             }
384 
385             // Create a configEntry so that the name is normalized.
386             ConfigEntry configEntry = new ConfigEntry(internal, key);
387 
388             ConfigEntryType type = configEntry.getType();
389 
390             ConfigEntry e = table.get(type);
391 
392             if (e == null) {
393                 if (type == null) {
394                     log.warn("Extra entry in " + internal + " of " + configEntry.getName());
395                     extra.put(key, configEntry);
396                 } else if (type.isSynthetic()) {
397                     log.warn("Ignoring unexpected entry in " + internal + " of " + configEntry.getName());
398                 } else {
399                     table.put(type, configEntry);
400                 }
401             } else {
402                 configEntry = e;
403             }
404 
405             buf.append(value);
406             getContinuation(configEntry, in, buf);
407 
408             // History is a special case it is of the form History_x.x
409             // The config entry is History without the x.x.
410             // We want to put x.x at the beginning of the string
411             value = buf.toString();
412             if (ConfigEntryType.HISTORY.equals(type)) {
413                 int pos = key.indexOf('_');
414                 value = key.substring(pos + 1) + ' ' + value;
415             }
416 
417             configEntry.addValue(value);
418         }
419     }
420 
421     private void loadInitials(BufferedReader in) throws IOException {
422         String initials = null;
423         while (true) {
424             String line = advance(in);
425             if (line == null) {
426                 break;
427             }
428 
429             if (line.charAt(0) == '[' && line.charAt(line.length() - 1) == ']') {
430                 // The conf file contains a leading line of the form [KJV]
431                 // This is the acronym by which Sword refers to it.
432                 initials = line.substring(1, line.length() - 1);
433                 break;
434             }
435         }
436         if (initials == null) {
437             log.error("Malformed conf file for " + internal + " no initials found. Using internal of " + internal);
438             initials = internal;
439         }
440         add(ConfigEntryType.INITIALS, initials);
441     }
442 
443     /**
444      * Get continuation lines, if any.
445      */
446     private void getContinuation(ConfigEntry configEntry, BufferedReader bin, StringBuilder buf) throws IOException {
447         for (String line = advance(bin); line != null; line = advance(bin)) {
448             int length = buf.length();
449 
450             // Look for bad data as this condition did exist
451             boolean continuation_expected = length > 0 && buf.charAt(length - 1) == '\\';
452 
453             if (continuation_expected) {
454                 // delete the continuation character
455                 buf.deleteCharAt(length - 1);
456             }
457 
458             if (isKeyLine(line)) {
459                 if (continuation_expected) {
460                     log.warn(report("Continuation followed by key for", configEntry.getName(), line));
461                 }
462 
463                 backup(line);
464                 break;
465             } else if (!continuation_expected) {
466                 log.warn(report("Line without previous continuation for", configEntry.getName(), line));
467             }
468 
469             if (!configEntry.allowsContinuation()) {
470                 log.warn(report("Ignoring unexpected additional line for", configEntry.getName(), line));
471             } else {
472                 if (continuation_expected) {
473                     buf.append('\n');
474                 }
475                 buf.append(line);
476             }
477         }
478     }
479 
480     /**
481      * Get the next line from the input
482      * 
483      * @param bin
484      *            The reader to get data from
485      * @return the next line
486      * @throws IOException
487      */
488     private String advance(BufferedReader bin) throws IOException {
489         // Was something put back? If so, return it.
490         if (readahead != null) {
491             String line = readahead;
492             readahead = null;
493             return line;
494         }
495 
496         // Get the next non-blank, non-comment line
497         String trimmed = null;
498         for (String line = bin.readLine(); line != null; line = bin.readLine()) {
499             // Remove trailing whitespace
500             trimmed = line.trim();
501 
502             int length = trimmed.length();
503 
504             // skip blank and comment lines
505             if (length != 0 && trimmed.charAt(0) != '#') {
506                 return trimmed;
507             }
508         }
509         return null;
510     }
511 
512     /**
513      * Read too far ahead and need to return a line.
514      */
515     private void backup(String oops) {
516         if (oops.length() > 0) {
517             readahead = oops;
518         } else {
519             // should never happen
520             log.error("Backup an empty string for " + internal);
521         }
522     }
523 
524     /**
525      * Does this line of text represent a key/value pair?
526      */
527     private boolean isKeyLine(String line) {
528         return KEY_VALUE_PATTERN.matcher(line).matches();
529     }
530 
531     /**
532      * A helper to create/replace a value for a given type.
533      * 
534      * @param type
535      * @param aValue
536      */
537     public void add(ConfigEntryType type, String aValue) {
538         table.put(type, new ConfigEntry(internal, type, aValue));
539     }
540 
541     private void adjustDataPath() {
542         String datapath = (String) getValue(ConfigEntryType.DATA_PATH);
543         if (datapath == null) {
544             datapath = "";
545         }
546         if (datapath.startsWith("./")) {
547             datapath = datapath.substring(2);
548         }
549         add(ConfigEntryType.DATA_PATH, datapath);
550     }
551 
552     private void adjustLanguage() {
553         Language lang = (Language) getValue(ConfigEntryType.LANG);
554         if (lang == null) {
555             lang = Language.DEFAULT_LANG;
556             add(ConfigEntryType.LANG, lang.toString());
557         }
558         testLanguage(internal, lang);
559 
560         Language langFrom = (Language) getValue(ConfigEntryType.GLOSSARY_FROM);
561         Language langTo = (Language) getValue(ConfigEntryType.GLOSSARY_TO);
562 
563         // If we have either langFrom or langTo, we are dealing with a glossary
564         if (langFrom != null || langTo != null) {
565             if (langFrom == null) {
566                 log.warn("Missing data for " + internal + ". Assuming " + ConfigEntryType.GLOSSARY_FROM.getName() + '=' + Languages.DEFAULT_LANG_CODE);
567                 langFrom = Language.DEFAULT_LANG;
568                 add(ConfigEntryType.GLOSSARY_FROM, lang.getCode());
569             }
570             testLanguage(internal, langFrom);
571 
572             if (langTo == null) {
573                 log.warn("Missing data for " + internal + ". Assuming " + ConfigEntryType.GLOSSARY_TO.getName() + '=' + Languages.DEFAULT_LANG_CODE);
574                 langTo = Language.DEFAULT_LANG;
575                 add(ConfigEntryType.GLOSSARY_TO, lang.getCode());
576             }
577             testLanguage(internal, langTo);
578 
579             // At least one of the two languages should match the lang entry
580             if (!langFrom.equals(lang) && !langTo.equals(lang)) {
581                 log.error("Data error in " + internal
582                           + ". Neither " + ConfigEntryType.GLOSSARY_FROM.getName()
583                           + " or " + ConfigEntryType.GLOSSARY_FROM.getName()
584                           + " match " + ConfigEntryType.LANG.getName());
585             } else if (!langFrom.equals(lang)) {
586                 // The LANG field should match the GLOSSARY_FROM field
587                 /*
588                  * log.error("Data error in " + internal + ". " +
589                  * ConfigEntryType.GLOSSARY_FROM.getName() + " ("
590                  * + langFrom.getCode() + ") does not match " +
591                  * ConfigEntryType.LANG.getName() + " (" + lang.getCode() +
592                  * ")");
593                  */
594                 lang = langFrom;
595                 add(ConfigEntryType.LANG, lang.getCode());
596             }
597         }
598     }
599 
600     private void adjustBookType() {
601         // The book type represents the underlying category of book.
602         // Fine tune it here.
603         BookCategory focusedCategory = (BookCategory) getValue(ConfigEntryType.CATEGORY);
604         questionable = focusedCategory == BookCategory.QUESTIONABLE;
605 
606         // From the config map, extract the important bean properties
607         String modTypeName = (String) getValue(ConfigEntryType.MOD_DRV);
608         if (modTypeName == null) {
609             log.error("Book not supported: malformed conf file for " + internal + " no " + ConfigEntryType.MOD_DRV.getName() + " found");
610             supported = false;
611             return;
612         }
613 
614         // At the moment only the KJV is supported.
615         String v11n = (String) getValue(ConfigEntryType.VERSIFICATION);
616         if (!Versifications.instance().isDefined(v11n)) {
617             supported = false;
618             return;
619         }
620         // Add the versification so that it can be part of the "props"
621         add(ConfigEntryType.VERSIFICATION, v11n);
622 
623         bookType = BookType.fromString(modTypeName);
624         if (getBookType() == null) {
625             log.error("Book not supported: malformed conf file for " + internal + " no book type found");
626             supported = false;
627             return;
628         }
629 
630         BookCategory basicCategory = getBookType().getBookCategory();
631         if (basicCategory == null) {
632             supported = false;
633             return;
634         }
635 
636         // The book type represents the underlying category of book.
637         // Fine tune it here.
638         if (focusedCategory == BookCategory.OTHER || focusedCategory == BookCategory.QUESTIONABLE) {
639             focusedCategory = getBookType().getBookCategory();
640         }
641 
642         add(ConfigEntryType.CATEGORY, focusedCategory.getName());
643     }
644 
645     private void adjustName() {
646         // If there is no name then use the internal name
647         if (table.get(ConfigEntryType.DESCRIPTION) == null) {
648             log.error("Malformed conf file for " + internal + " no " + ConfigEntryType.DESCRIPTION.getName() + " found. Using internal of " + internal);
649             add(ConfigEntryType.DESCRIPTION, internal);
650         }
651     }
652 
653     /**
654      * Determine which books are not supported. Also, report on problems.
655      */
656     private void validate() {
657         // if (isEnciphered())
658         // {
659         //            log.debug("Book not supported: " + internal + " because it is locked and there is no key.");
660         // supported = false;
661         // return;
662         // }
663     }
664 
665     private void testLanguage(String initials, Language lang) {
666         if (!lang.isValidLanguage()) {
667             log.warn("Unknown language " + lang.getCode() + " in book " + initials);
668         }
669     }
670 
671     /**
672      * Build an ordered map so that it displays in a consistent order.
673      */
674     private void toOSIS(OSISUtil.OSISFactory factory, Element ele, String aTitle, ConfigEntryType[] category) {
675         Element title = null;
676         for (int i = 0; i < category.length; i++) {
677             ConfigEntry entry = table.get(category[i]);
678             Element configElement = null;
679 
680             if (entry != null) {
681                 configElement = entry.toOSIS();
682             }
683 
684             if (title == null && configElement != null) {
685                 // I18N(DMS): use aTitle to lookup translation.
686                 title = factory.createHeader();
687                 title.addContent(aTitle);
688                 ele.addContent(title);
689             }
690 
691             if (configElement != null) {
692                 ele.addContent(configElement);
693             }
694         }
695     }
696 
697     private void toConf(StringBuilder buf, ConfigEntryType[] category) {
698         for (int i = 0; i < category.length; i++) {
699 
700             ConfigEntry entry = table.get(category[i]);
701 
702             if (entry != null && !entry.getType().isSynthetic()) {
703                 String text = entry.toConf();
704                 if (text != null && text.length() > 0) {
705                     buf.append(entry.toConf());
706                 }
707             }
708         }
709     }
710 
711     /**
712      * Build an ordered map so that it displays in a consistent order.
713      */
714     private void toOSIS(OSISUtil.OSISFactory factory, Element ele, String aTitle, Map<String, ConfigEntry> map) {
715         Element title = null;
716         for (Map.Entry<String, ConfigEntry> mapEntry : map.entrySet()) {
717             ConfigEntry entry = mapEntry.getValue();
718             Element configElement = null;
719 
720             if (entry != null) {
721                 configElement = entry.toOSIS();
722             }
723 
724             if (title == null && configElement != null) {
725                 // I18N(DMS): use aTitle to lookup translation.
726                 title = factory.createHeader();
727                 title.addContent(aTitle);
728                 ele.addContent(title);
729             }
730 
731             if (configElement != null) {
732                 ele.addContent(configElement);
733             }
734         }
735     }
736 
737     private void toConf(StringBuilder buf, Map<String, ConfigEntry> map) {
738         for (Map.Entry<String, ConfigEntry> mapEntry : map.entrySet()) {
739             ConfigEntry entry = mapEntry.getValue();
740             String text = entry.toConf();
741             if (text != null && text.length() > 0) {
742                 buf.append(text);
743             }
744         }
745     }
746 
747     private String report(String issue, String confEntryName, String line) {
748         StringBuilder buf = new StringBuilder(100);
749         buf.append(issue);
750         buf.append(' ');
751         buf.append(confEntryName);
752         buf.append(" in ");
753         buf.append(internal);
754         buf.append(": ");
755         buf.append(line);
756 
757         return buf.toString();
758     }
759 
760     /**
761      * Sword only recognizes two encodings for its modules: UTF-8 and LATIN1
762      * Sword uses MS Windows cp1252 for Latin 1 not the standard. Arrgh!
763      */
764     private static final String ENCODING_UTF8 = "UTF-8";
765     private static final String ENCODING_LATIN1 = "WINDOWS-1252";
766 
767     /**
768      * These are the elements that JSword requires. They are a superset of those
769      * that Sword requires.
770      */
771     /*
772      * For documentation purposes at this time.
773      * private static final ConfigEntryType[] REQUIRED = {
774      *         ConfigEntryType.INITIALS,
775      *         ConfigEntryType.DESCRIPTION,
776      *         ConfigEntryType.CATEGORY, // may not be present in conf
777      *         ConfigEntryType.DATA_PATH,
778      *         ConfigEntryType.MOD_DRV,
779      * };
780      */
781 
782     private static final ConfigEntryType[] BASIC_INFO = {
783             ConfigEntryType.INITIALS,
784             ConfigEntryType.DESCRIPTION,
785             ConfigEntryType.CATEGORY,
786             ConfigEntryType.LCSH,
787             ConfigEntryType.SWORD_VERSION_DATE,
788             ConfigEntryType.VERSION,
789             ConfigEntryType.HISTORY,
790             ConfigEntryType.OBSOLETES,
791             ConfigEntryType.INSTALL_SIZE,
792     };
793 
794     private static final ConfigEntryType[] LANG_INFO = {
795             ConfigEntryType.LANG,
796             ConfigEntryType.GLOSSARY_FROM,
797             ConfigEntryType.GLOSSARY_TO,
798     };
799 
800     private static final ConfigEntryType[] COPYRIGHT_INFO = {
801             ConfigEntryType.ABOUT,
802             ConfigEntryType.SHORT_PROMO,
803             ConfigEntryType.DISTRIBUTION_LICENSE,
804             ConfigEntryType.DISTRIBUTION_NOTES,
805             ConfigEntryType.DISTRIBUTION_SOURCE,
806             ConfigEntryType.SHORT_COPYRIGHT,
807             ConfigEntryType.COPYRIGHT,
808             ConfigEntryType.COPYRIGHT_DATE,
809             ConfigEntryType.COPYRIGHT_HOLDER,
810             ConfigEntryType.COPYRIGHT_CONTACT_NAME,
811             ConfigEntryType.COPYRIGHT_CONTACT_ADDRESS,
812             ConfigEntryType.COPYRIGHT_CONTACT_EMAIL,
813             ConfigEntryType.COPYRIGHT_CONTACT_NOTES,
814             ConfigEntryType.COPYRIGHT_NOTES,
815             ConfigEntryType.TEXT_SOURCE,
816     };
817 
818     private static final ConfigEntryType[] FEATURE_INFO = {
819             ConfigEntryType.FEATURE,
820             ConfigEntryType.GLOBAL_OPTION_FILTER,
821             ConfigEntryType.FONT,
822     };
823 
824     private static final ConfigEntryType[] SYSTEM_INFO = {
825             ConfigEntryType.DATA_PATH,
826             ConfigEntryType.MOD_DRV,
827             ConfigEntryType.SOURCE_TYPE,
828             ConfigEntryType.BLOCK_TYPE,
829             ConfigEntryType.BLOCK_COUNT,
830             ConfigEntryType.COMPRESS_TYPE,
831             ConfigEntryType.ENCODING,
832             ConfigEntryType.MINIMUM_VERSION,
833             ConfigEntryType.OSIS_VERSION,
834             ConfigEntryType.OSIS_Q_TO_TICK,
835             ConfigEntryType.DIRECTION,
836             ConfigEntryType.KEY_TYPE,
837             ConfigEntryType.DISPLAY_LEVEL,
838             ConfigEntryType.VERSIFICATION,
839     };
840 
841     private static final ConfigEntryType[] HIDDEN = {
842         ConfigEntryType.CIPHER_KEY,
843     };
844 
845     /**
846      * The log stream
847      */
848     private static final Logger log = Logger.getLogger(ConfigEntryTable.class);
849 
850     /**
851      * The original name of this config file from mods.d. This is only used for
852      * managing warnings and errors
853      */
854     private String internal;
855 
856     /**
857      * A map of lists of known config entries.
858      */
859     private Map<ConfigEntryType, ConfigEntry> table;
860 
861     /**
862      * A map of lists of unknown config entries.
863      */
864     private Map<String, ConfigEntry> extra;
865 
866     /**
867      * The BookType for this ConfigEntry
868      */
869     private BookType bookType;
870 
871     /**
872      * True if this book's config type can be used by JSword.
873      */
874     private boolean supported;
875 
876     /**
877      * True if this book is considered questionable.
878      */
879     private boolean questionable;
880 
881     /**
882      * A helper for the reading of the conf file.
883      */
884     private String readahead;
885 
886     /**
887      * If the module's config is tied to a file remember it so that it can be
888      * updated.
889      */
890     private File configFile;
891 
892     /**
893      * Pattern that matches a key=value. The key can contain ascii letters,
894      * numbers, underscore and period. The key must begin at the beginning of
895      * the line. The = sign following the key may be surrounded by whitespace.
896      * The value may contain anything, including an = sign.
897      */
898     private static final Pattern KEY_VALUE_PATTERN = Pattern.compile("^([A-Za-z0-9_.]+)\\s*=\\s*(.*)$");
899 
900 }
901