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