1
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
72 public final class ConfigEntryTable {
73
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
93 public void load(File file) throws IOException {
94 configFile = file;
95
96 BufferedReader in = null;
97 int bufferSize = 8192;
98 try {
99 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
139 public void load(byte[] buffer) throws IOException {
140 BufferedReader in = null;
141 try {
142 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
177 public boolean isQuestionable() {
178 return questionable;
179 }
180
181
184 public boolean isSupported() {
185 return supported;
186 }
187
188
193 public boolean isEnciphered() {
194 String cipher = (String) getValue(ConfigEntryType.CIPHER_KEY);
195 return cipher != null;
196 }
197
198
203 public boolean isLocked() {
204 String cipher = (String) getValue(ConfigEntryType.CIPHER_KEY);
205 return cipher != null && cipher.length() == 0;
206 }
207
208
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 Reporter.informUser(this, JSMsg.gettext("Unable to save the book's unlock key."));
228 }
229 }
230 return true;
231 }
232
233
238 public String getUnlockKey() {
239 return (String) getValue(ConfigEntryType.CIPHER_KEY);
240 }
241
242
245 public Set<ConfigEntryType> getKeys() {
246 return table.keySet();
247 }
248
249
252 public Set<String> getExtraKeys() {
253 return extra.keySet();
254 }
255
256
259 public BookType getBookType() {
260 return bookType;
261 }
262
263
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
289 public boolean match(ConfigEntryType type, String search) {
290 ConfigEntry ce = table.get(type);
291 return ce != null && ce.match(search);
292 }
293
294
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
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 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 buf.setLength(0);
360
361 String line = advance(in);
362 if (line == null) {
363 break;
364 }
365
366 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 if (value.length() == 0 && !ConfigEntryType.CIPHER_KEY.getName().equals(key)) {
381 log.warn("Ignoring empty entry in " + internal + ": " + line);
382 continue;
383 }
384
385 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 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 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
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 boolean continuation_expected = length > 0 && buf.charAt(length - 1) == '\\';
452
453 if (continuation_expected) {
454 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
488 private String advance(BufferedReader bin) throws IOException {
489 if (readahead != null) {
491 String line = readahead;
492 readahead = null;
493 return line;
494 }
495
496 String trimmed = null;
498 for (String line = bin.readLine(); line != null; line = bin.readLine()) {
499 trimmed = line.trim();
501
502 int length = trimmed.length();
503
504 if (length != 0 && trimmed.charAt(0) != '#') {
506 return trimmed;
507 }
508 }
509 return null;
510 }
511
512
515 private void backup(String oops) {
516 if (oops.length() > 0) {
517 readahead = oops;
518 } else {
519 log.error("Backup an empty string for " + internal);
521 }
522 }
523
524
527 private boolean isKeyLine(String line) {
528 return KEY_VALUE_PATTERN.matcher(line).matches();
529 }
530
531
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 (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 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
594 lang = langFrom;
595 add(ConfigEntryType.LANG, lang.getCode());
596 }
597 }
598 }
599
600 private void adjustBookType() {
601 BookCategory focusedCategory = (BookCategory) getValue(ConfigEntryType.CATEGORY);
604 questionable = focusedCategory == BookCategory.QUESTIONABLE;
605
606 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 String v11n = (String) getValue(ConfigEntryType.VERSIFICATION);
616 if (!Versifications.instance().isDefined(v11n)) {
617 supported = false;
618 return;
619 }
620 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 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 (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
656 private void validate() {
657 }
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
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 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
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 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
764 private static final String ENCODING_UTF8 = "UTF-8";
765 private static final String ENCODING_LATIN1 = "WINDOWS-1252";
766
767
771
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
848 private static final Logger log = Logger.getLogger(ConfigEntryTable.class);
849
850
854 private String internal;
855
856
859 private Map<ConfigEntryType, ConfigEntry> table;
860
861
864 private Map<String, ConfigEntry> extra;
865
866
869 private BookType bookType;
870
871
874 private boolean supported;
875
876
879 private boolean questionable;
880
881
884 private String readahead;
885
886
890 private File configFile;
891
892
898 private static final Pattern KEY_VALUE_PATTERN = Pattern.compile("^([A-Za-z0-9_.]+)\\s*=\\s*(.*)$");
899
900 }
901