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.common.util;
21  
22  import java.text.MessageFormat;
23  import java.util.HashMap;
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.icu.NumberShaper;
30  import org.crosswire.jsword.internationalisation.LocaleProviderManager;
31  import org.slf4j.LoggerFactory;
32  
33  /**
34   * A base class for implementing type safe internationalization (i18n) that is
35   * easy for most cases.
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 class MsgBase {
42      /**
43       * Create a MsgBase object
44       */
45      protected MsgBase() {
46          this.shaper = new NumberShaper();
47      }
48  
49      /**
50       * Get the internationalized text, but return key if key is unknown.
51       *
52       * @param key the format key to internationalize
53       * @param params the parameters for the format
54       * @return the internationalized text
55       */
56      public String lookup(String key, Object... params) {
57          String rawMessage = obtainString(key);
58          if (params.length == 0) {
59              return shaper.shape(rawMessage);
60          }
61  
62          //MessageFormat strips off all single apostrophes from the message so replace single quotes with two quotes
63          rawMessage = rawMessage.replaceAll("'", "''");
64  
65          return shaper.shape(MessageFormat.format(rawMessage, params));
66      }
67  
68      private String obtainString(String key) {
69          try {
70              if (getLocalisedResources() != null) {
71                  return getLocalisedResources().getString(key);
72              }
73          } catch (MissingResourceException ex) {
74              log.error("Missing resource: Locale={} name={} package={}", LocaleProviderManager.getLocale(), key, getClass().getName());
75          }
76  
77          return key;
78      }
79  
80      private ResourceBundle getLocalisedResources() {
81          Class<? extends MsgBase> implementingClass = getClass();
82          String className = implementingClass.getName();
83          String shortClassName = ClassUtil.getShortClassName(className);
84  
85          Locale currentUserLocale = LocaleProviderManager.getLocale();
86          Map<String, ResourceBundle> localisedResourceMap = getLazyLocalisedResourceMap(currentUserLocale);
87  
88          ResourceBundle resourceBundle = localisedResourceMap.get(className);
89          if (resourceBundle == null) {
90              resourceBundle = getResourceBundleForClass(implementingClass, className, shortClassName, currentUserLocale, localisedResourceMap);
91          }
92  
93          // if for some reason, we are still looking at a null, then we can only do our best, which is to return the English Locale.
94          if (resourceBundle == null) {
95              resourceBundle  = getResourceBundleForClass(implementingClass, className, shortClassName, Locale.ENGLISH, localisedResourceMap);
96          }
97  
98          //if we're still looking at a null, there is definitely nothing else we can do, so throw an exception
99          if (resourceBundle == null) {
100             log.error("Missing resources: Locale={} class={}", currentUserLocale, className);
101             throw new MissingResourceException("Unable to find the language resources.", className, shortClassName);
102         }
103         return resourceBundle;
104     }
105 
106     /**
107      * Gets the resource bundle for a particular class
108      *
109      * @param implementingClass the implementing class
110      * @param className the class name
111      * @param shortClassName the short class name
112      * @param currentUserLocale the current user locale
113      * @param localisedResourceMap the localised resource map
114      * @return the resource bundle for class
115      */
116     private ResourceBundle getResourceBundleForClass(Class<? extends MsgBase> implementingClass, String className, String shortClassName, Locale currentUserLocale, Map<String, ResourceBundle> localisedResourceMap) {
117         ResourceBundle resourceBundle;
118         synchronized (MsgBase.class) {
119             resourceBundle = localisedResourceMap.get(className);
120             if (resourceBundle == null) {
121                 try {
122                     resourceBundle = ResourceBundle.getBundle(shortClassName, currentUserLocale, CWClassLoader.instance(implementingClass));
123                     localisedResourceMap.put(className, resourceBundle);
124                 } catch (MissingResourceException ex) {
125                     log.warn("Assuming key is the default message {}", className);
126                 }
127             }
128         }
129         return resourceBundle;
130     }
131 
132     /**
133      * Gets the localised resource map, initialising it if it doesn't already exist
134      *
135      * @param currentUserLocale the current user locale
136      * @return the lazy localised resource map
137      */
138     private Map<String, ResourceBundle> getLazyLocalisedResourceMap(Locale currentUserLocale) {
139         Map<String, ResourceBundle> localisedResourceMap = localeToResourceMap.get(currentUserLocale);
140         if (localisedResourceMap == null) {
141             synchronized (MsgBase.class) {
142                 localisedResourceMap = localeToResourceMap.get(currentUserLocale);
143                 if (localisedResourceMap == null) {
144                     localisedResourceMap = new HashMap<String, ResourceBundle>(512);
145                     localeToResourceMap.put(currentUserLocale, localisedResourceMap);
146                 }
147             }
148         }
149         return localisedResourceMap;
150     }
151 
152     private static Map<Locale, Map<String, ResourceBundle>> localeToResourceMap = new HashMap<Locale, Map<String, ResourceBundle>>();
153 
154     /** Internationalize numbers */
155     private NumberShaper shaper;
156 
157     /**
158      * The log stream
159      */
160     private static final org.slf4j.Logger log = LoggerFactory.getLogger(MsgBase.class);
161 }
162