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.versification;
21  
22  import java.util.HashMap;
23  import java.util.LinkedHashMap;
24  import java.util.Locale;
25  import java.util.Map;
26  import java.util.MissingResourceException;
27  import java.util.ResourceBundle;
28  
29  import org.crosswire.common.util.CWClassLoader;
30  import org.crosswire.common.util.ClassUtil;
31  import org.crosswire.common.util.StringUtil;
32  import org.crosswire.jsword.internationalisation.LocaleProviderManager;
33  
34  /**
35   * BibleNames deals with locale sensitive BibleBook name lookup conversions.
36   * 
37   * @see gnu.lgpl.License The GNU Lesser General Public License for details.
38   * @author Joe Walker
39   * @author DM Smith
40   */
41  public final class BibleNames {
42      /**
43       * Get the singleton instance of BibleNames.
44       *
45       * @return the singleton
46       */
47      public static BibleNames instance() {
48          return instance;
49      }
50  
51      /**
52       * Get the BookName.
53       *
54       * @param book the desired book
55       * @return The requested BookName or null if not in this versification
56       */
57      public BookName getBookName(BibleBook book) {
58          return getLocalizedBibleNames().getBookName(book);
59      }
60  
61      /**
62       * Get the preferred name of a book. Altered by the case setting (see
63       * setBookCase() and isFullBookName())
64       *
65       * @param book the desired book
66       * @return The full name of the book or blank if not in this versification
67       */
68      public String getPreferredName(BibleBook book) {
69          return getLocalizedBibleNames().getPreferredName(book);
70      }
71  
72      /**
73       * Get the full name of a book (e.g. "Genesis"). Altered by the case setting
74       * (see setBookCase())
75       *
76       * @param book the book
77       * @return The full name of the book or blank if not in this versification
78       */
79      public String getLongName(BibleBook book) {
80          return getLocalizedBibleNames().getLongName(book);
81      }
82  
83      /**
84       * Get the short name of a book (e.g. "Gen"). Altered by the case setting
85       * (see setBookCase())
86       *
87       * @param book the book
88       * @return The short name of the book or blank if not in this versification
89       */
90      public String getShortName(BibleBook book) {
91          return getLocalizedBibleNames().getShortName(book);
92      }
93  
94      /**
95       * Get a book from its name.
96       *
97       * @param find
98       *            The string to identify
99       * @return The BibleBook, On error null
100      */
101     public BibleBook getBook(String find) {
102         BibleBook book = null;
103         if (containsLetter(find)) {
104             book = BibleBook.fromOSIS(find);
105 
106             if (book == null) {
107                 book = getLocalizedBibleNames().getBook(find, false);
108             }
109 
110             if (book == null) {
111                 book = englishBibleNames.getBook(find, false);
112             }
113 
114             if (book == null) {
115                 book = getLocalizedBibleNames().getBook(find, true);
116             }
117 
118             if (book == null) {
119                 book = englishBibleNames.getBook(find, true);
120             }
121         }
122 
123         return book;
124     }
125 
126     /**
127      * Is the given string a valid book name. If this method returns true then
128      * getBook() will return a BibleBook and not null.
129      *
130      * @param find
131      *            The string to identify
132      * @return true when the book name is recognized
133      */
134     public boolean isBook(String find) {
135         return getBook(find) != null;
136     }
137 
138     /**
139      * Load name information for BibleNames for a given locale.
140      * This routine is for testing the underlying NameList.
141      * 
142      * @param locale
143      */
144     void load(Locale locale) {
145         NameList bibleNames = new NameList(locale);
146         if (localizedBibleNames.get(locale) == null) {
147             localizedBibleNames.put(locale, bibleNames);
148         }
149     }
150 
151     /**
152      * This class is a singleton, enforced by a private constructor.
153      */
154     private BibleNames() {
155         localizedBibleNames = new HashMap<Locale, NameList>();
156         englishBibleNames = getBibleNamesForLocale(Locale.ENGLISH);
157         localizedBibleNames.put(Locale.ENGLISH, englishBibleNames);
158     }
159 
160     /**
161      * Gets the localized bible names, based on the {@link LocaleProviderManager}
162      *
163      * @return the localized bible names
164      */
165     private NameList getLocalizedBibleNames() {
166         // Get the current Locale
167         return getBibleNamesForLocale(LocaleProviderManager.getLocale());
168     }
169 
170     /**
171      * Gets the bible names for a specific locale.
172      *
173      * @param locale the locale
174      * @return the bible names for locale
175      */
176     private NameList getBibleNamesForLocale(Locale locale) {
177         NameList bibleNames = localizedBibleNames.get(locale);
178         if (bibleNames == null) {
179             bibleNames = new NameList(locale);
180             localizedBibleNames.put(locale, bibleNames);
181         }
182 
183         return bibleNames;
184     }
185 
186     /**
187      * This is simply a convenience function to wrap Character.isLetter()
188      *
189      * @param text
190      *            The string to be parsed
191      * @return true if the string contains letters
192      */
193     private static boolean containsLetter(String text) {
194         for (int i = 0; i < text.length(); i++) {
195             if (Character.isLetter(text.charAt(i))) {
196                 return true;
197             }
198         }
199 
200         return false;
201     }
202 
203     /**
204      * NameList is the internal, internationalize list of names
205      * for a locale.
206      *
207      * @see gnu.lgpl.License The GNU Lesser General Public License for details.<br>
208      *      The copyright to this program is held by its authors.
209      * @author DM Smith
210      */
211     private class NameList {
212         /**
213          * Create NameList for the given locale
214          */
215         NameList(Locale locale) {
216             this.locale = locale;
217             initialize();
218         }
219 
220         BookName getBookName(BibleBook book) {
221             return books.get(book);
222         }
223 
224         /**
225          * Get the preferred name of a book. Altered by the case setting (see
226          * setBookCase() and isFullBookName())
227          * 
228          * @param book
229          *            The book of the Bible
230          * @return The full name of the book
231          */
232         String getPreferredName(BibleBook book) {
233             return getBookName(book).getPreferredName();
234         }
235 
236         /**
237          * Get the full name of a book (e.g. "Genesis"). Altered by the case
238          * setting (see setBookCase())
239          * 
240          * @param book
241          *            The book of the Bible
242          * @return The full name of the book
243          */
244         String getLongName(BibleBook book) {
245             return getBookName(book).getLongName();
246         }
247 
248         /**
249          * Get the short name of a book (e.g. "Gen"). Altered by the case
250          * setting (see setBookCase())
251          * 
252          * @param book
253          *            The book of the Bible
254          * @return The short name of the book
255          */
256         String getShortName(BibleBook book) {
257             return getBookName(book).getShortName();
258         }
259 
260         /**
261          * Get a book from its name.
262          * 
263          * @param find
264          *            The string to identify
265          * @param fuzzy
266          *            Whether to also find bible books where only a substring matches
267          * @return The BibleBook, On error null
268          */
269         BibleBook getBook(String find, boolean fuzzy) {
270             String match = BookName.normalize(find, locale);
271 
272             BookName bookName = fullNT.get(match);
273             if (bookName != null) {
274                 return bookName.getBook();
275             }
276 
277             bookName = shortNT.get(match);
278             if (bookName != null) {
279                 return bookName.getBook();
280             }
281 
282             bookName = altNT.get(match);
283             if (bookName != null) {
284                 return bookName.getBook();
285             }
286 
287             bookName = fullOT.get(match);
288             if (bookName != null) {
289                 return bookName.getBook();
290             }
291 
292             bookName = shortOT.get(match);
293             if (bookName != null) {
294                 return bookName.getBook();
295             }
296 
297             bookName = altOT.get(match);
298             if (bookName != null) {
299                 return bookName.getBook();
300             }
301 
302             bookName = fullNC.get(match);
303             if (bookName != null) {
304                 return bookName.getBook();
305             }
306 
307             bookName = shortNC.get(match);
308             if (bookName != null) {
309                 return bookName.getBook();
310             }
311 
312             bookName = altNC.get(match);
313             if (bookName != null) {
314                 return bookName.getBook();
315             }
316 
317             if (!fuzzy) {
318                 return null;
319             }
320 
321             for (BookName aBook : books.values()) {
322                 if (aBook.match(match)) {
323                     return aBook.getBook();
324                 }
325             }
326 
327             return null;
328         }
329 
330         /**
331          * Load up the resources for Bible book and section names, and cache the
332          * upper and lower versions of them.
333          */
334         private void initialize() {
335             int ntCount = 0;
336             int otCount = 0;
337             int ncCount = 0;
338             BibleBook[] bibleBooks = BibleBook.values();
339             for (BibleBook book : bibleBooks) {
340                 int ordinal = book.ordinal();
341                 if (ordinal > BibleBook.INTRO_OT.ordinal() && ordinal < BibleBook.INTRO_NT.ordinal()) {
342                     ++ntCount;
343                 } else if (ordinal > BibleBook.INTRO_NT.ordinal() && ordinal <= BibleBook.REV.ordinal()) {
344                     ++otCount;
345                 } else {
346                     ++ncCount;
347                 }
348             }
349 
350             // Create the book name maps
351             books = new LinkedHashMap<BibleBook, BookName>(ntCount + otCount + ncCount);
352 
353             String className = BibleNames.class.getName();
354             String shortClassName = ClassUtil.getShortClassName(className);
355             ResourceBundle resources = ResourceBundle.getBundle(shortClassName, locale, CWClassLoader.instance(BibleNames.class));
356 
357             fullNT = new HashMap<String, BookName>(ntCount);
358             shortNT = new HashMap<String, BookName>(ntCount);
359             altNT = new HashMap<String, BookName>(ntCount);
360             for (int i = BibleBook.MATT.ordinal(); i <= BibleBook.REV.ordinal(); ++i) {
361                 BibleBook book = bibleBooks[i];
362                 store(resources, book, fullNT, shortNT, altNT);
363             }
364 
365             fullOT = new HashMap<String, BookName>(otCount);
366             shortOT = new HashMap<String, BookName>(otCount);
367             altOT = new HashMap<String, BookName>(otCount);
368             for (int i = BibleBook.GEN.ordinal(); i <= BibleBook.MAL.ordinal(); ++i) {
369                 BibleBook book = bibleBooks[i];
370                 store(resources, book, fullOT, shortOT, altOT);
371             }
372 
373             fullNC = new HashMap<String, BookName>(ncCount);
374             shortNC = new HashMap<String, BookName>(ncCount);
375             altNC = new HashMap<String, BookName>(ncCount);
376             store(resources, BibleBook.INTRO_BIBLE, fullNC, shortNC, altNC);
377             store(resources, BibleBook.INTRO_OT, fullNC, shortNC, altNC);
378             store(resources, BibleBook.INTRO_NT, fullNC, shortNC, altNC);
379             for (int i = BibleBook.REV.ordinal() + 1; i < bibleBooks.length; ++i) {
380                 BibleBook book = bibleBooks[i];
381                 store(resources, book, fullNC, shortNC, altNC);
382             }
383         }
384 
385         private void store(ResourceBundle resources, BibleBook book, Map fullMap, Map shortMap, Map altMap) {
386             String osisName = book.getOSIS();
387 
388             String fullBook = getString(resources, osisName + FULL_KEY);
389 
390             String shortBook = getString(resources, osisName + SHORT_KEY);
391             if (shortBook.length() == 0) {
392                 shortBook = fullBook;
393             }
394 
395             String altBook = getString(resources, osisName + ALT_KEY);
396 
397             BookName bookName = new BookName(locale, BibleBook.fromOSIS(osisName), fullBook, shortBook, altBook);
398             books.put(book, bookName);
399 
400             fullMap.put(bookName.getNormalizedLongName(), bookName);
401 
402             shortMap.put(bookName.getNormalizedShortName(), bookName);
403 
404             String[] alternates = StringUtil.split(BookName.normalize(altBook, locale), ',');
405 
406             for (int j = 0; j < alternates.length; j++) {
407                 altMap.put(alternates[j], bookName);
408             }
409         }
410 
411         /*
412          * Helper to make the code more readable.
413          */
414         private String getString(ResourceBundle resources, String key) {
415             try {
416                 return resources.getString(key);
417             } catch (MissingResourceException e) {
418                 assert false;
419             }
420             return null;
421         }
422 
423         private static final String FULL_KEY = ".Full";
424         private static final String SHORT_KEY = ".Short";
425         private static final String ALT_KEY = ".Alt";
426 
427         /** The locale for the Bible Names */
428         private Locale locale;
429 
430         /**
431          * The collection of BookNames by BibleBooks.
432          */
433         private LinkedHashMap<BibleBook, BookName> books;
434 
435         /**
436          * The full names of the New Testament books of the Bible normalized,
437          * generated at runtime
438          */
439         private Map<String, BookName> fullNT;
440 
441         /**
442          * The full names of the Old Testament books of the Bible normalized,
443          * generated at runtime
444          */
445         private Map<String, BookName> fullOT;
446 
447         /**
448          * The full names of the Deuterocanonical books of the Bible normalized,
449          * generated at runtime
450          */
451         private Map<String, BookName> fullNC;
452 
453         /**
454          * Standard shortened names for the New Testament books of the Bible,
455          * normalized, generated at runtime.
456          */
457         private Map<String, BookName> shortNT;
458 
459         /**
460          * Standard shortened names for the Old Testament books of the Bible
461          * normalized, generated at runtime.
462          */
463         private Map<String, BookName> shortOT;
464 
465         /**
466          * Standard shortened names for the Deuterocanonical books of the Bible
467          * normalized, generated at runtime.
468          */
469         private Map<String, BookName> shortNC;
470 
471         /**
472          * Alternative shortened names for the New Testament books of the Bible
473          * normalized, generated at runtime.
474          */
475         private Map<String, BookName> altNT;
476 
477         /**
478          * Alternative shortened names for the Old Testament books of the Bible
479          * normalized, generated at runtime.
480          */
481         private Map<String, BookName> altOT;
482 
483         /**
484          * Alternative shortened names for the Deuterocanonical books of the
485          * Bible normalized, generated at runtime.
486          */
487         private Map<String, BookName> altNC;
488     }
489 
490     /** we cache the Localized Bible Names because there is quite a bit of processing going on for each individual Locale */
491     private transient Map<Locale, NameList> localizedBibleNames;
492 
493     /** English BibleNames, or null when using the program's default locale */
494     private static NameList englishBibleNames;
495 
496     private static final BibleNames instance = new BibleNames();
497 }
498