001 package org.hd.d.pg2k.svrCore;
002
003 import java.io.IOException;
004 import java.io.InvalidObjectException;
005 import java.io.ObjectInputStream;
006 import java.io.Serializable;
007 import java.text.MessageFormat;
008 import java.util.Locale;
009 import java.util.MissingResourceException;
010 import java.util.ResourceBundle;
011 import java.util.concurrent.ConcurrentHashMap;
012 import java.util.concurrent.ConcurrentMap;
013
014 /**
015 * Created by IntelliJ IDEA.
016 * User: DHD
017 * Date: 04-Sep-2006
018 * Time: 16:34:48
019 * To change this template use File | Settings | File Templates.
020 */
021 public class LocaleBeanBase implements Serializable
022 {
023 /**``Safe'' locale is the one we want all activity to default to.
024 * This locale is one that we should always be able to support.
025 */
026 protected static final Locale SAFE_LOCALE = I18NTools.DEFAULT_SYSTEM_LOCALE;
027
028 /**Locale retrieved by setRequest, or a safe locale value; never null.
029 * Volatile to allow thread-safe access without a lock.
030 */
031 private volatile Locale userLocale;
032
033 /**Retrieve the current locale; never null. */
034 public Locale getLocale() { return(userLocale); }
035
036 /**Mechanism by which deriving classes can set the locale (never null). */
037 protected void setLocale(final Locale l) { assert(l != null); userLocale = l; }
038
039 // /**If true then monitor missing i18n support.
040 // * This helps us prioritise translation work!
041 // * <p>
042 // * Note that this may be relatively expensive to enable.
043 // */
044 // private static final boolean MONITOR_MISSING_I18N = true;
045 //
046 // /**If true then count missing message translations in supported languages.
047 // * This helps us prioritise translation work!
048 // * <p>
049 // * Note that this may be relatively expensive to enable.
050 // */
051 // private static final boolean COUNT_MISSING_TRANSLATIONS = true;
052
053 /**Public no-arg constructor for ease of use as a JavaBean.
054 * Sets to a safe locale.
055 */
056 public LocaleBeanBase()
057 { this(SAFE_LOCALE); }
058
059 /**Public no-arg constructor for ease of use as a JavaBean.
060 * This defers as much work as it reasonably can.
061 *
062 * @param l initial locale; never null
063 */
064 public LocaleBeanBase(final Locale l)
065 {
066 userLocale = l;
067
068 // Verify object state.
069 try { validateObject(); }
070 catch(final InvalidObjectException e)
071 { throw new IllegalArgumentException(e.getMessage()); }
072 }
073
074 /**Human-readable text representation; never null. */
075 @Override public String toString() { return(userLocale.toString()); }
076
077 /**Estimated maximum number of real/used common resource bundles available; strictly positive.
078 * Should ideally be a power of two a little larger than the true value,
079 * or at least of those used other than very infrequently,
080 * for maximum efficiency.
081 */
082 private static final int EST_COMMON_BUNDLES = 16;
083
084 /**Cache mapping from the actual common bundle locale to the bundle itself.
085 * Assumed to be bounded in size by the actual concrete common bundles available.
086 * <p>
087 * We expect to need relatively little write concurrency, even at start-up.
088 */
089 private static final ConcurrentMap<Locale, ResourceBundle> _cachedCommonBundles =
090 new ConcurrentHashMap<Locale, ResourceBundle>(EST_COMMON_BUNDLES, 0.7f, 4);
091
092 /**Private LRU cache from requested common locale to actual locale.
093 * Should be large enough to keep the hit rate acceptable,
094 * but otherwise effectively size-bounded with unused items purged.
095 * <p>
096 * Sized on the basis that there may be up to 10s of 'alias' locales
097 * for each actual supported common bundle locale,
098 * eg many 'en'/'fr'/'de'/'es' variants requested for each actually supported.
099 */
100 private static final MemoryTools.SimpleLRUMapAutoSizeForHitRate<Locale, Locale> _cacheCommonRealLocales =
101 MemoryTools.SimpleLRUMapAutoSizeForHitRate.<Locale, Locale>create(0.01f, EST_COMMON_BUNDLES, 8*EST_COMMON_BUNDLES, 0.75f, "_cacheCommonRealLocales");
102
103 /**Gets the (properties-only) common resource bundle for the given locale.
104 * Assume that this is safe since the bundle should be immutable.
105 * <p>
106 * Canonicalises the requested locale to a concrete available common bundle,
107 * and then looks that up to get the bundle.
108 * <p>
109 * Both parts of this lookup are cached.
110 * <p>
111 * Thread-safe and allows concurrency.
112 * <p>
113 * We do not search for class-based resources.
114 */
115 public static ResourceBundle getCommonResourceBundle(final Locale l)
116 {
117 // Look up real locale; fall through if not cached.
118 final Locale realLocale = _cacheCommonRealLocales.get(l);
119 if(null != realLocale)
120 {
121 final ResourceBundle result = _cachedCommonBundles.get(realLocale);
122 if(null != result)
123 { return(result); } // Got result from cache.
124 }
125
126 // Fetch and permanently cache the resource bundle under its real locale.
127 // We only provide a properties-based version of the common bundle.
128 final ResourceBundle result = ResourceBundle.getBundle(I18NTools.BUNDLE_COMMON, l,
129 ResourceBundle.Control.getControl(ResourceBundle.Control.FORMAT_PROPERTIES));
130 final Locale realLocaleFound = result.getLocale();
131 _cachedCommonBundles.putIfAbsent(realLocaleFound, result);
132 _cacheCommonRealLocales.put(l, realLocaleFound); // Note the locale mapping from requested to real.
133 return(result);
134 }
135
136 /**Gets the appropriate localised message from the common set.
137 * In case of a missing message this returns the key itself.
138 * <p>
139 * If the name is null, the result is null.
140 */
141 public String getLocalisedMessage(final String msgName)
142 {
143 if(msgName == null) { return(null); }
144 try
145 {
146 // Fetch the common resource bundle and look up our message.
147 final ResourceBundle bundle = getCommonResourceBundle(userLocale);
148
149 // Get the localised message, if it exists.
150 final String localisedMsg = bundle.getString(msgName);
151
152 // if(MONITOR_MISSING_I18N)
153 // {
154 // // Note a completely unsupported language (we had to fall back).
155 // final Locale bundleLocale = bundle.getLocale();
156 // final String bundleLang = bundleLocale.getLanguage();
157 // if(!bundleLang.equals(userLocale.getLanguage()))
158 // { StatsLogger.captureDataPoint(LocaleBean.statsIDLang, "MISSING-LANG-" + userLocale.getLanguage()); }
159 // // Note if we are missing specific translations
160 // // for an otherwise-supported language.
161 // // THIS MAY BE EXPENSIVE.
162 // else if((COUNT_MISSING_TRANSLATIONS) &&
163 // (!SAFE_LOCALE.getLanguage().equals(bundleLang)))
164 // {
165 // // This assumes that the translation will NOT be the same as
166 // // the original fallback-language version!
167 // if(localisedMsg.equals(getCommonResourceBundle(SAFE_LOCALE).getString(msgName)))
168 // { StatsLogger.captureDataPoint(LocaleBean.statsIDLang, "MISSING-TRANS-" + bundleLang + '-' + msgName); }
169 // }
170 // }
171
172 return(localisedMsg);
173 }
174 catch(final MissingResourceException e)
175 {
176 // // Note missing message...
177 // if(MONITOR_MISSING_I18N)
178 // { StatsLogger.captureDataPoint(LocaleBean.statsIDLang, "MISSING-MSG-" + msgName); }
179 return(msgName);
180 }
181 }
182
183 /**Get localised message from the common set with embedded formatting to apply to its argument(s).
184 * In case of a missing message this returns the key itself.
185 * <p>
186 * If the name is null, the result is null.
187 */
188 public String getLocalisedMessage(final String msgName, final Object ... args)
189 {
190 if(msgName == null) { return(null); }
191 final String fmt = getLocalisedMessage(msgName);
192 return((new MessageFormat(fmt, userLocale)).format(args)); // Use the correct locale...
193 // return(java.text.MessageFormat.format(fmt, args)); // Use the server's locale.
194 }
195
196 // /**Gets the tree-description resource bundle for the given locale.
197 // * Assume that the built-in caching is sufficient,
198 // * and that this is safe since the bundle should be immutable.
199 // */
200 // public ResourceBundle getTreeDescResourceBundle(final Locale l)
201 // {
202 // return(ResourceBundle.getBundle(I18NTools.BUNDLE_TREEDESC, l));
203 // }
204 //
205 // /**Gets the appropriate localised tree-description message; never null unless the key is null.
206 // * In case of a missing message this returns the key itself.
207 // * <p>
208 // * The key is converted to lower-case (if necessary) before lookup.
209 // * <p>
210 // * If the name is null, the result is null.
211 // */
212 // public String getLocalisedTreeDescMessage(final String msgName)
213 // {
214 // if(msgName == null) { return(null); }
215 // try {
216 // // Fetch the common resource bundle and look up our message.
217 // final ResourceBundle b = getTreeDescResourceBundle(userLocale);
218 // return(b.getString(msgName.toLowerCase()));
219 // }
220 // catch(final MissingResourceException e)
221 // { return(msgName); }
222 // }
223
224 /**Deserialise. */
225 private void readObject(final ObjectInputStream in)
226 throws IOException, ClassNotFoundException
227 {
228 in.defaultReadObject();
229 validateObject(); // Validate state immediately.
230 }
231
232 /**Validate fields/state.
233 * Called in the constructor and possibly after de-serialising.
234 * <p>
235 * Barf if something bad is found.
236 * (Maybe allow some extra info in debug version.)
237 */
238 public void validateObject()
239 throws InvalidObjectException
240 {
241 // Check that all components are sane and safe.
242 if(userLocale == null)
243 { throw new InvalidObjectException("bad object: userLocale == null"); }
244 }
245
246 /**Unique Serialisation class ID generated by http://random.hd.org/. */
247 private static final long serialVersionUID = -6180248508840300658L;
248 }