001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.utils;
014
015import static org.apache.juneau.internal.StringUtils.*;
016import static org.apache.juneau.internal.ThrowableUtils.*;
017
018import java.text.*;
019import java.util.*;
020import java.util.concurrent.*;
021
022import org.apache.juneau.*;
023
024/**
025 * Wraps a {@link ResourceBundle} to provide some useful additional functionality.
026 *
027 * <ul class='spaced-list'>
028 *    <li>
029 *       Instead of throwing {@link MissingResourceException}, the {@link #getString(String)} method
030 *       will return <js>"{!!key}"</js> if the bundle was not found, and <js>"{!key}"</js> if bundle
031 *       was found but the key is not in the bundle.
032 *    <li>
033 *       A client locale can be set as a {@link ThreadLocal} object using the static {@link #setClientLocale(Locale)}
034 *       so that client localized messages can be retrieved using the {@link #getClientString(String, Object...)}
035 *       method on all instances of this class.
036 *    <li>
037 *       Resource bundles on parent classes can be added to the search path for this class by using the
038 *       {@link #addSearchPath(Class, String)} method.
039 *       This allows messages to be retrieved from the resource bundles of parent classes.
040 *    <li>
041 *       Locale-specific bundles can be retrieved by using the {@link #getBundle(Locale)} method.
042 *    <li>
043 *       The {@link #getString(Locale, String, Object...)} method can be used to retrieve locale-specific messages.
044 *    <li>
045 *       Messages in the resource bundle can optionally be prefixed with the simple class name.
046 *       For example, if the class is <code>MyClass</code> and the properties file contains <js>"MyClass.myMessage"</js>,
047 *       the message can be retrieved using <code>getString(<js>"myMessage"</js>)</code>.
048 * </ul>
049 *
050 * <h5 class='section'>Notes:</h5>
051 * <ul class='spaced-list'>
052 *    <li>
053 *       This class is thread-safe.
054 * </ul>
055 */
056public class MessageBundle extends ResourceBundle {
057
058   private static final ThreadLocal<Locale> clientLocale = new ThreadLocal<>();
059
060   private final ResourceBundle rb;
061   private final String bundlePath, className;
062   private final Class<?> forClass;
063   private final long creationThreadId;
064
065   // A map that contains all keys [shortKeyName->keyName] and [keyName->keyName], where shortKeyName
066   // refers to keys prefixed and stripped of the class name (e.g. "foobar"->"MyClass.foobar")
067   private final Map<String,String> keyMap = new ConcurrentHashMap<>();
068
069   // Contains all keys present in all bundles in searchBundles.
070   private final ConcurrentSkipListSet<String> allKeys = new ConcurrentSkipListSet<>();
071
072   // Bundles to search through to find properties.
073   // Typically this will be a list of resource bundles for each class up the class hierarchy chain.
074   private final CopyOnWriteArrayList<MessageBundle> searchBundles = new CopyOnWriteArrayList<>();
075
076   // Cache of message bundles per locale.
077   private final ConcurrentHashMap<Locale,MessageBundle> localizedBundles = new ConcurrentHashMap<>();
078
079   /**
080    * Sets the locale for this thread so that calls to {@link #getClientString(String, Object...)} return messages in
081    * that locale.
082    *
083    * @param locale The new client locale.
084    */
085   public static void setClientLocale(Locale locale) {
086      MessageBundle.clientLocale.set(locale);
087   }
088
089   /**
090    * Constructor.
091    *
092    * <p>
093    * When this method is used, the bundle path is determined by searching for the resource bundle
094    * in the following locations:
095    * <ul>
096    *    <li><code>[package].ForClass.properties</code>
097    *    <li><code>[package].nls.ForClass.properties</code>
098    *    <li><code>[package].i18n.ForClass.properties</code>
099    * </ul>
100    *
101    * @param forClass The class
102    * @return A new message bundle belonging to the class.
103    */
104   public static final MessageBundle create(Class<?> forClass) {
105      return create(forClass, findBundlePath(forClass));
106   }
107
108   /**
109    * Constructor.
110    *
111    * <p>
112    * A shortcut for calling <code>new MessageBundle(forClass, bundlePath)</code>.
113    *
114    * @param forClass The class
115    * @param bundlePath The location of the resource bundle.
116    * @return A new message bundle belonging to the class.
117    */
118   public static final MessageBundle create(Class<?> forClass, String bundlePath) {
119      return new MessageBundle(forClass, bundlePath);
120   }
121
122   private static final String findBundlePath(Class<?> forClass) {
123      String path = forClass.getName();
124      if (tryBundlePath(forClass, path))
125         return path;
126      path = forClass.getPackage().getName() + ".nls." + forClass.getSimpleName();
127      if (tryBundlePath(forClass, path))
128         return path;
129      path = forClass.getPackage().getName() + ".i18n." + forClass.getSimpleName();
130      if (tryBundlePath(forClass, path))
131         return path;
132      return null;
133   }
134
135   private static final boolean tryBundlePath(Class<?> c, String path) {
136      try {
137         path = c.getName();
138         ResourceBundle.getBundle(path, Locale.getDefault(), c.getClassLoader());
139         return true;
140      } catch (MissingResourceException e) {
141         return false;
142      }
143   }
144
145   /**
146    * Constructor.
147    *
148    * @param forClass The class using this resource bundle.
149    * @param bundlePath
150    *    The path of the resource bundle to wrap.
151    *    This can be an absolute path (e.g. <js>"com.foo.MyMessages"</js>) or a path relative to the package of the
152    *    <l>forClass</l> (e.g. <js>"MyMessages"</js> if <l>forClass</l> is <js>"com.foo.MyClass"</js>).
153    */
154   public MessageBundle(Class<?> forClass, String bundlePath) {
155      this(forClass, bundlePath, Locale.getDefault());
156   }
157
158   private MessageBundle(Class<?> forClass, String bundlePath, Locale locale) {
159      this.forClass = forClass;
160      this.className = forClass.getSimpleName();
161      if (bundlePath.endsWith(".properties"))
162         throw new RuntimeException("Bundle path should not end with '.properties'");
163      this.bundlePath = bundlePath;
164      this.creationThreadId = Thread.currentThread().getId();
165      ClassLoader cl = forClass.getClassLoader();
166      ResourceBundle trb = null;
167      try {
168         trb = ResourceBundle.getBundle(bundlePath, locale, cl);
169      } catch (MissingResourceException e) {
170         try {
171            trb = ResourceBundle.getBundle(forClass.getPackage().getName() + '.' + bundlePath, locale, cl);
172         } catch (MissingResourceException e2) {
173         }
174      }
175      this.rb = trb;
176      if (rb != null) {
177
178         // Populate keyMap with original mappings.
179         for (Enumeration<String> e = getKeys(); e.hasMoreElements();) {
180            String key = e.nextElement();
181            keyMap.put(key, key);
182         }
183
184         // Override/augment with shortname mappings (e.g. "foobar"->"MyClass.foobar")
185         String c = className + '.';
186         for (Enumeration<String> e = getKeys(); e.hasMoreElements();) {
187            String key = e.nextElement();
188            if (key.startsWith(c)) {
189               String shortKey = key.substring(className.length() + 1);
190               keyMap.put(shortKey, key);
191            }
192         }
193
194         allKeys.addAll(keyMap.keySet());
195      }
196      searchBundles.add(this);
197   }
198
199
200   /**
201    * Add another bundle path to this resource bundle.
202    *
203    * <p>
204    * Order of property lookup is first-to-last.
205    *
206    * <p>
207    * This method must be called from the same thread as the call to the constructor.
208    * This eliminates the need for synchronization.
209    *
210    * @param forClass The class using this resource bundle.
211    * @param bundlePath The bundle path.
212    * @return This object (for method chaining).
213    */
214   public MessageBundle addSearchPath(Class<?> forClass, String bundlePath) {
215      assertSameThread(creationThreadId, "This method can only be called from the same thread that created the object.");
216      MessageBundle srb = new MessageBundle(forClass, bundlePath);
217      if (srb.rb != null) {
218         allKeys.addAll(srb.keySet());
219         searchBundles.add(srb);
220      }
221      return this;
222   }
223
224   @Override /* ResourceBundle */
225   public boolean containsKey(String key) {
226      return allKeys.contains(key);
227   }
228
229   /**
230    * Similar to {@link ResourceBundle#getString(String)} except allows you to pass in {@link MessageFormat} objects.
231    *
232    * @param key The resource bundle key.
233    * @param args Optional {@link MessageFormat}-style arguments.
234    * @return
235    *    The resolved value.  Never <jk>null</jk>.
236    *    <js>"{!!key}"</js> if the bundle is missing.
237    *    <js>"{!key}"</js> if the key is missing.
238    */
239   public String getString(String key, Object...args) {
240      String s = getString(key);
241      if (s.length() > 0 && s.charAt(0) == '{')
242         return s;
243      return format(s, args);
244   }
245
246   /**
247    * Same as {@link #getString(String, Object...)} but allows you to specify the locale.
248    *
249    * @param locale The locale of the resource bundle to retrieve message from.
250    * @param key The resource bundle key.
251    * @param args Optional {@link MessageFormat}-style arguments.
252    * @return
253    *    The resolved value.  Never <jk>null</jk>.
254    *    <js>"{!!key}"</js> if the bundle is missing.
255    *    <js>"{!key}"</js> if the key is missing.
256    */
257   public String getString(Locale locale, String key, Object...args) {
258      if (locale == null)
259         return getString(key, args);
260      return getBundle(locale).getString(key, args);
261   }
262
263   /**
264    * Same as {@link #getString(String, Object...)} but uses the locale specified on the call to {@link #setClientLocale(Locale)}.
265    *
266    * @param key The resource bundle key.
267    * @param args Optional {@link MessageFormat}-style arguments.
268    * @return
269    *    The resolved value.  Never <jk>null</jk>.
270    *    <js>"{!!key}"</js> if the bundle is missing.
271    *    <js>"{!key}"</js> if the key is missing.
272    */
273   public String getClientString(String key, Object...args) {
274      return getString(clientLocale.get(), key, args);
275   }
276
277   /**
278    * Looks for all the specified keys in the resource bundle and returns the first value that exists.
279    *
280    * @param keys The list of possible keys.
281    * @return The resolved value, or <jk>null</jk> if no value is found or the resource bundle is missing.
282    */
283   public String findFirstString(String...keys) {
284      if (rb == null)
285         return null;
286      for (String k : keys) {
287         if (containsKey(k))
288            return getString(k);
289      }
290      return null;
291   }
292
293   /**
294    * Same as {@link #findFirstString(String...)}, but uses the specified locale.
295    *
296    * @param locale The locale of the resource bundle to retrieve message from.
297    * @param keys The list of possible keys.
298    * @return The resolved value, or <jk>null</jk> if no value is found or the resource bundle is missing.
299    */
300   public String findFirstString(Locale locale, String...keys) {
301      MessageBundle srb = getBundle(locale);
302      return srb.findFirstString(keys);
303   }
304
305   @Override /* ResourceBundle */
306   public Set<String> keySet() {
307      return Collections.unmodifiableSet(allKeys);
308   }
309
310   /**
311    * Returns all keys in this resource bundle with the specified prefix.
312    *
313    * @param prefix The prefix.
314    * @return The set of all keys in the resource bundle with the prefix.
315    */
316   public Set<String> keySet(String prefix) {
317      Set<String> set = new HashSet<>();
318      for (String s : keySet()) {
319         if (s.equals(prefix) || (s.startsWith(prefix) && s.charAt(prefix.length()) == '.'))
320            set.add(s);
321      }
322      return set;
323   }
324
325   @Override /* ResourceBundle */
326   public Enumeration<String> getKeys() {
327      if (rb == null)
328         return new Vector<String>(0).elements();
329      return rb.getKeys();
330   }
331
332   @Override /* ResourceBundle */
333   protected Object handleGetObject(String key) {
334      for (MessageBundle srb : searchBundles) {
335         if (srb.rb != null) {
336            String key2 = srb.keyMap.get(key);
337            if (key2 != null) {
338               try {
339                  return srb.rb.getObject(key2);
340               } catch (Exception e) {
341                  return "{!"+key+"}";
342               }
343            }
344         }
345      }
346      if (rb == null)
347         return "{!!"+key+"}";
348      return "{!"+key+"}";
349   }
350
351   /**
352    * Returns this resource bundle as an {@link ObjectMap}.
353    *
354    * <p>
355    * Useful for debugging purposes.
356    * Note that any class that implements a <code>swap()</code> method will automatically be serialized by
357    * calling this method and serializing the result.
358    *
359    * <p>
360    * This method always constructs a new {@link ObjectMap} on each call.
361    *
362    * @return A new map containing all the keys and values in this bundle.
363    */
364   public ObjectMap swap() {
365      ObjectMap om = new ObjectMap();
366      for (String k : allKeys)
367         om.put(k, getString(k));
368      return om;
369   }
370
371   /**
372    * Returns the resource bundle for the specified locale.
373    *
374    * @param locale The client locale.
375    * @return The resource bundle for the specified locale.  Never <jk>null</jk>.
376    */
377   public MessageBundle getBundle(Locale locale) {
378      MessageBundle mb = localizedBundles.get(locale);
379      if (mb != null)
380         return mb;
381      mb = new MessageBundle(forClass, bundlePath, locale);
382      List<MessageBundle> l = new ArrayList<>(searchBundles.size()-1);
383      for (int i = 1; i < searchBundles.size(); i++) {
384         MessageBundle srb = searchBundles.get(i);
385         srb = new MessageBundle(srb.forClass, srb.bundlePath, locale);
386         l.add(srb);
387         mb.allKeys.addAll(srb.keySet());
388      }
389      mb.searchBundles.addAll(l);
390      localizedBundles.putIfAbsent(locale, mb);
391      return localizedBundles.get(locale);
392   }
393}