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      this.bundlePath = bundlePath;
162      this.creationThreadId = Thread.currentThread().getId();
163      ClassLoader cl = forClass.getClassLoader();
164      ResourceBundle trb = null;
165      try {
166         trb = ResourceBundle.getBundle(bundlePath, locale, cl);
167      } catch (MissingResourceException e) {
168         try {
169            trb = ResourceBundle.getBundle(forClass.getPackage().getName() + '.' + bundlePath, locale, cl);
170         } catch (MissingResourceException e2) {
171         }
172      }
173      this.rb = trb;
174      if (rb != null) {
175
176         // Populate keyMap with original mappings.
177         for (Enumeration<String> e = getKeys(); e.hasMoreElements();) {
178            String key = e.nextElement();
179            keyMap.put(key, key);
180         }
181
182         // Override/augment with shortname mappings (e.g. "foobar"->"MyClass.foobar")
183         String c = className + '.';
184         for (Enumeration<String> e = getKeys(); e.hasMoreElements();) {
185            String key = e.nextElement();
186            if (key.startsWith(c)) {
187               String shortKey = key.substring(className.length() + 1);
188               keyMap.put(shortKey, key);
189            }
190         }
191
192         allKeys.addAll(keyMap.keySet());
193      }
194      searchBundles.add(this);
195   }
196
197
198   /**
199    * Add another bundle path to this resource bundle.
200    * 
201    * <p>
202    * Order of property lookup is first-to-last.
203    * 
204    * <p>
205    * This method must be called from the same thread as the call to the constructor.
206    * This eliminates the need for synchronization.
207    * 
208    * @param forClass The class using this resource bundle.
209    * @param bundlePath The bundle path.
210    * @return This object (for method chaining).
211    */
212   public MessageBundle addSearchPath(Class<?> forClass, String bundlePath) {
213      assertSameThread(creationThreadId, "This method can only be called from the same thread that created the object.");
214      MessageBundle srb = new MessageBundle(forClass, bundlePath);
215      if (srb.rb != null) {
216         allKeys.addAll(srb.keySet());
217         searchBundles.add(srb);
218      }
219      return this;
220   }
221
222   @Override /* ResourceBundle */
223   public boolean containsKey(String key) {
224      return allKeys.contains(key);
225   }
226
227   /**
228    * Similar to {@link ResourceBundle#getString(String)} except allows you to pass in {@link MessageFormat} objects.
229    * 
230    * @param key The resource bundle key.
231    * @param args Optional {@link MessageFormat}-style arguments.
232    * @return
233    *    The resolved value.  Never <jk>null</jk>.
234    *    <js>"{!!key}"</js> if the bundle is missing.
235    *    <js>"{!key}"</js> if the key is missing.
236    */
237   public String getString(String key, Object...args) {
238      String s = getString(key);
239      if (s.length() > 0 && s.charAt(0) == '{')
240         return s;
241      return format(s, args);
242   }
243
244   /**
245    * Same as {@link #getString(String, Object...)} but allows you to specify the locale.
246    * 
247    * @param locale The locale of the resource bundle to retrieve message from.
248    * @param key The resource bundle key.
249    * @param args Optional {@link MessageFormat}-style arguments.
250    * @return
251    *    The resolved value.  Never <jk>null</jk>.
252    *    <js>"{!!key}"</js> if the bundle is missing.
253    *    <js>"{!key}"</js> if the key is missing.
254    */
255   public String getString(Locale locale, String key, Object...args) {
256      if (locale == null)
257         return getString(key, args);
258      return getBundle(locale).getString(key, args);
259   }
260
261   /**
262    * Same as {@link #getString(String, Object...)} but uses the locale specified on the call to {@link #setClientLocale(Locale)}.
263    * 
264    * @param key The resource bundle key.
265    * @param args Optional {@link MessageFormat}-style arguments.
266    * @return
267    *    The resolved value.  Never <jk>null</jk>.
268    *    <js>"{!!key}"</js> if the bundle is missing.
269    *    <js>"{!key}"</js> if the key is missing.
270    */
271   public String getClientString(String key, Object...args) {
272      return getString(clientLocale.get(), key, args);
273   }
274
275   /**
276    * Looks for all the specified keys in the resource bundle and returns the first value that exists.
277    * 
278    * @param keys The list of possible keys.
279    * @return The resolved value, or <jk>null</jk> if no value is found or the resource bundle is missing.
280    */
281   public String findFirstString(String...keys) {
282      if (rb == null)
283         return null;
284      for (String k : keys) {
285         if (containsKey(k))
286            return getString(k);
287      }
288      return null;
289   }
290
291   /**
292    * Same as {@link #findFirstString(String...)}, but uses the specified locale.
293    * 
294    * @param locale The locale of the resource bundle to retrieve message from.
295    * @param keys The list of possible keys.
296    * @return The resolved value, or <jk>null</jk> if no value is found or the resource bundle is missing.
297    */
298   public String findFirstString(Locale locale, String...keys) {
299      MessageBundle srb = getBundle(locale);
300      return srb.findFirstString(keys);
301   }
302
303   @Override /* ResourceBundle */
304   public Set<String> keySet() {
305      return Collections.unmodifiableSet(allKeys);
306   }
307
308   /**
309    * Returns all keys in this resource bundle with the specified prefix.
310    * 
311    * @param prefix The prefix.
312    * @return The set of all keys in the resource bundle with the prefix.
313    */
314   public Set<String> keySet(String prefix) {
315      Set<String> set = new HashSet<>();
316      for (String s : keySet()) {
317         if (s.equals(prefix) || (s.startsWith(prefix) && s.charAt(prefix.length()) == '.'))
318            set.add(s);
319      }
320      return set;
321   }
322
323   @Override /* ResourceBundle */
324   public Enumeration<String> getKeys() {
325      if (rb == null)
326         return new Vector<String>(0).elements();
327      return rb.getKeys();
328   }
329
330   @Override /* ResourceBundle */
331   protected Object handleGetObject(String key) {
332      for (MessageBundle srb : searchBundles) {
333         if (srb.rb != null) {
334            String key2 = srb.keyMap.get(key);
335            if (key2 != null) {
336               try {
337                  return srb.rb.getObject(key2);
338               } catch (Exception e) {
339                  return "{!"+key+"}";
340               }
341            }
342         }
343      }
344      if (rb == null)
345         return "{!!"+key+"}";
346      return "{!"+key+"}";
347   }
348
349   /**
350    * Returns this resource bundle as an {@link ObjectMap}.
351    * 
352    * <p>
353    * Useful for debugging purposes.
354    * Note that any class that implements a <code>swap()</code> method will automatically be serialized by
355    * calling this method and serializing the result.
356    * 
357    * <p>
358    * This method always constructs a new {@link ObjectMap} on each call.
359    * 
360    * @return A new map containing all the keys and values in this bundle.
361    */
362   public ObjectMap swap() {
363      ObjectMap om = new ObjectMap();
364      for (String k : allKeys)
365         om.put(k, getString(k));
366      return om;
367   }
368
369   /**
370    * Returns the resource bundle for the specified locale.
371    * 
372    * @param locale The client locale.
373    * @return The resource bundle for the specified locale.  Never <jk>null</jk>.
374    */
375   public MessageBundle getBundle(Locale locale) {
376      MessageBundle mb = localizedBundles.get(locale);
377      if (mb != null)
378         return mb;
379      mb = new MessageBundle(forClass, bundlePath, locale);
380      List<MessageBundle> l = new ArrayList<>(searchBundles.size()-1);
381      for (int i = 1; i < searchBundles.size(); i++) {
382         MessageBundle srb = searchBundles.get(i);
383         srb = new MessageBundle(srb.forClass, srb.bundlePath, locale);
384         l.add(srb);
385         mb.allKeys.addAll(srb.keySet());
386      }
387      mb.searchBundles.addAll(l);
388      localizedBundles.putIfAbsent(locale, mb);
389      return localizedBundles.get(locale);
390   }
391}