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        }