001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.cp;
018
019import static org.apache.juneau.common.utils.StringUtils.*;
020import static org.apache.juneau.common.utils.ThrowableUtils.*;
021import static org.apache.juneau.common.utils.Utils.*;
022import static org.apache.juneau.internal.CollectionUtils.*;
023import static org.apache.juneau.internal.ResourceBundleUtils.*;
024
025import java.text.*;
026import java.util.*;
027import java.util.concurrent.*;
028
029import org.apache.juneau.*;
030import org.apache.juneau.collections.*;
031import org.apache.juneau.common.utils.*;
032import org.apache.juneau.internal.*;
033import org.apache.juneau.marshaller.*;
034import org.apache.juneau.parser.ParseException;
035import org.apache.juneau.utils.*;
036
037/**
038 * An enhanced {@link ResourceBundle}.
039 *
040 * <p>
041 * Wraps a ResourceBundle to provide some useful additional functionality.
042 *
043 * <ul>
044 *    <li>
045 *       Instead of throwing {@link MissingResourceException}, the {@link ResourceBundle#getString(String)} method
046 *       will return <js>"{!key}"</js> if the message could not be found.
047 *    <li>
048 *       Supported hierarchical lookup of resources from parent parent classes.
049 *    <li>
050 *       Support for easy retrieval of localized bundles.
051 *    <li>
052 *       Support for generalized resource bundles (e.g. properties files containing keys for several classes).
053 * </ul>
054 *
055 * <p>
056 * The following example shows the basic usage of this class for retrieving localized messages:
057 *
058 * <p class='bini'>
059 *    <cc># Contents of MyClass.properties</cc>
060 *    <ck>foo</ck> = <cv>foo {0}</cv>
061 *    <ck>MyClass.bar</ck> = <cv>bar {0}</cv>
062 * </p>
063 * <p class='bjava'>
064 *    <jk>public class</jk> MyClass {
065 *       <jk>private static final</jk> Messages <jsf>MESSAGES</jsf> = Messages.<jsm>of</jsm>(MyClass.<jk>class</jk>);
066 *
067 *       <jk>public void</jk> doFoo() {
068 *
069 *       <jc>// A normal property.</jc>
070 *          String <jv>foo</jv> = <jsf>MESSAGES</jsf>.getString(<js>"foo"</js>,<js>"x"</js>);  <jc>// == "foo x"</jc>
071 *
072 *          <jc>// A property prefixed by class name.</jc>
073 *          String <jv>bar</jv> = <jsf>MESSAGES</jsf>.getString(<js>"bar"</js>,<js>"x"</js>);  <jc>// == "bar x"</jc>
074 *
075 *          <jc>// A non-existent property.</jc>
076 *          String <jv>baz</jv> = <jsf>MESSAGES</jsf>.getString(<js>"baz"</js>,<js>"x"</js>);  <jc>// == "{!baz}"</jc>
077 *       }
078 *    }
079 * </p>
080 *
081 * <p>
082 *    The ability to resolve keys prefixed by class name allows you to place all your messages in a single file such
083 *    as a common <js>"Messages.properties"</js> file along with those for other classes.
084 * <p>
085 *    The following shows how to retrieve messages from a common bundle:
086 *
087 * <p class='bjava'>
088 *    <jk>public class</jk> MyClass {
089 *       <jk>private static final</jk> Messages <jsf>MESSAGES</jsf> = Messages.<jsm>of</jsm>(MyClass.<jk>class</jk>, <js>"Messages"</js>);
090 *    }
091 * </p>
092 *
093 * <p>
094 *    Resource bundles are searched using the following base name patterns:
095 *    <ul>
096 *       <li><js>"{package}.{name}"</js>
097 *       <li><js>"{package}.i18n.{name}"</js>
098 *       <li><js>"{package}.nls.{name}"</js>
099 *       <li><js>"{package}.messages.{name}"</js>
100 *    </ul>
101 *
102 * <p>
103 *    These patterns can be customized using the {@link Builder#baseNames(String...)} method.
104 *
105 * <p>
106 *    Localized messages can be retrieved in the following way:
107 *
108 * <p class='bjava'>
109 *    <jc>// Return value from Japan locale bundle.</jc>
110 *    String <jv>foo</jv> = <jsf>MESSAGES</jsf>.forLocale(Locale.<jsf>JAPAN</jsf>).getString(<js>"foo"</js>);
111 * </p>
112 *
113 * <h5 class='section'>See Also:</h5><ul>
114 * </ul>
115 */
116public class Messages extends ResourceBundle {
117
118   //-----------------------------------------------------------------------------------------------------------------
119   // Static
120   //-----------------------------------------------------------------------------------------------------------------
121
122   /**
123    * Static creator.
124    *
125    * @param forClass
126    *    The class we're creating this object for.
127    * @return A new builder.
128    */
129   public static final Builder create(Class<?> forClass) {
130      return new Builder(forClass);
131   }
132
133   /**
134    * Constructor.
135    *
136    * @param forClass
137    *    The class we're creating this object for.
138    * @return A new message bundle belonging to the class.
139    */
140   public static final Messages of(Class<?> forClass) {
141      return create(forClass).build();
142   }
143
144   /**
145    * Constructor.
146    *
147    * @param forClass
148    *    The class we're creating this object for.
149    * @param name
150    *    The bundle name (e.g. <js>"Messages"</js>).
151    *    <br>If <jk>null</jk>, uses the class name.
152    * @return A new message bundle belonging to the class.
153    */
154   public static final Messages of(Class<?> forClass, String name) {
155      return create(forClass).name(name).build();
156   }
157
158   //-----------------------------------------------------------------------------------------------------------------
159   // Builder
160   //-----------------------------------------------------------------------------------------------------------------
161
162   /**
163    * Builder class.
164    */
165   public static class Builder extends BeanBuilder<Messages> {
166
167      Class<?> forClass;
168      Locale locale;
169      String name;
170      Messages parent;
171      List<Tuple2<Class<?>,String>> locations;
172
173      private String[] baseNames = {"{package}.{name}","{package}.i18n.{name}","{package}.nls.{name}","{package}.messages.{name}"};
174
175      /**
176       * Constructor.
177       *
178       * @param forClass The base class.
179       */
180      protected Builder(Class<?> forClass) {
181         super(Messages.class, BeanStore.INSTANCE);
182         this.forClass = forClass;
183         this.name = forClass.getSimpleName();
184         locations = Utils.list();
185         locale = Locale.getDefault();
186      }
187
188      @Override /* BeanBuilder */
189      protected Messages buildDefault() {
190
191         if (! locations.isEmpty()) {
192            Tuple2<Class<?>,String>[] mbl = locations.toArray(new Tuple2[0]);
193
194            Builder x = null;
195
196            for (int i = mbl.length-1; i >= 0; i--) {
197               Class<?> c = Utils.firstNonNull(mbl[i].getA(), forClass);
198               String value = mbl[i].getB();
199               if (isJsonObject(value, true)) {
200                  MessagesString ms;
201                  try {
202                     ms = Json5.DEFAULT.read(value, MessagesString.class);
203                  } catch (ParseException e) {
204                     throw asRuntimeException(e);
205                  }
206                  x = Messages.create(c).name(ms.name).baseNames(Utils.splita(ms.baseNames, ',')).locale(ms.locale).parent(x == null ? null : x.build());
207               } else {
208                  x = Messages.create(c).name(value).parent(x == null ? null : x.build());
209               }
210            }
211
212            return x == null ? null : x.build();  // Shouldn't be null.
213         }
214
215         return new Messages(this);
216      }
217
218      private static class MessagesString {
219         public String name;
220         public String[] baseNames;
221         public String locale;
222      }
223
224      //-------------------------------------------------------------------------------------------------------------
225      // Properties
226      //-------------------------------------------------------------------------------------------------------------
227
228      /**
229       * Adds a parent bundle.
230       *
231       * @param parent The parent bundle.  Can be <jk>null</jk>.
232       * @return This object.
233       */
234      public Builder parent(Messages parent) {
235         this.parent = parent;
236         return this;
237      }
238
239      /**
240       * Specifies the bundle name (e.g. <js>"Messages"</js>).
241       *
242       * @param name
243       *    The bundle name.
244       *    <br>If <jk>null</jk>, the forClass class name is used.
245       * @return This object.
246       */
247      public Builder name(String name) {
248         this.name = Utils.isEmpty(name) ? forClass.getSimpleName() : name;
249         return this;
250      }
251
252      /**
253       * Specifies the base name patterns to use for finding the resource bundle.
254       *
255       * @param baseNames
256       *    The bundle base names.
257       *    <br>The default is the following:
258       *    <ul>
259       *       <li><js>"{package}.{name}"</js>
260       *       <li><js>"{package}.i18n.{name}"</js>
261       *       <li><js>"{package}.nls.{name}"</js>
262       *       <li><js>"{package}.messages.{name}"</js>
263       *    </ul>
264       * @return This object.
265       */
266      public Builder baseNames(String...baseNames) {
267         this.baseNames = baseNames == null ? new String[]{} : baseNames;
268         return this;
269      }
270
271      /**
272       * Specifies the locale.
273       *
274       * @param locale
275       *    The locale.
276       *    If <jk>null</jk>, the default locale is used.
277       * @return This object.
278       */
279      public Builder locale(Locale locale) {
280         this.locale = locale == null ? Locale.getDefault() : locale;
281         return this;
282      }
283
284      /**
285       * Specifies the locale.
286       *
287       * @param locale
288       *    The locale.
289       *    If <jk>null</jk>, the default locale is used.
290       * @return This object.
291       */
292      public Builder locale(String locale) {
293         return locale(locale == null ? null : Locale.forLanguageTag(locale));
294      }
295
296      /**
297       * Specifies a location of where to look for messages.
298       *
299       * @param baseClass The base class.
300       * @param bundlePath The bundle path.
301       * @return This object.
302       */
303      public Builder location(Class<?> baseClass, String bundlePath) {
304         this.locations.add(0, Tuple2.of(baseClass, bundlePath));
305         return this;
306      }
307
308      /**
309       * Specifies a location of where to look for messages.
310       *
311       * @param bundlePath The bundle path.
312       * @return This object.
313       */
314      public Builder location(String bundlePath) {
315         this.locations.add(0, Tuple2.of(forClass, bundlePath));
316         return this;
317      }
318      @Override /* Overridden from BeanBuilder */
319      public Builder impl(Object value) {
320         super.impl(value);
321         return this;
322      }
323
324      @Override /* Overridden from BeanBuilder */
325      public Builder type(Class<?> value) {
326         super.type(value);
327         return this;
328      }
329      //-------------------------------------------------------------------------------------------------------------
330      // Other methods
331      //-------------------------------------------------------------------------------------------------------------
332
333      ResourceBundle getBundle() {
334         ClassLoader cl = forClass.getClassLoader();
335         JsonMap m = JsonMap.of("name", name, "package", forClass.getPackage().getName());
336         for (String bn : baseNames) {
337            bn = StringUtils.replaceVars(bn, m);
338            ResourceBundle rb = findBundle(bn, locale, cl);
339            if (rb != null)
340               return rb;
341         }
342         return null;
343      }
344   }
345
346   //-----------------------------------------------------------------------------------------------------------------
347   // Instance
348   //-----------------------------------------------------------------------------------------------------------------
349
350   private ResourceBundle rb;
351   private Class<?> c;
352   private Messages parent;
353   private Locale locale;
354
355   // Cache of message bundles per locale.
356   private final ConcurrentHashMap<Locale,Messages> localizedMessages = new ConcurrentHashMap<>();
357
358   // Cache of virtual keys to actual keys.
359   private final Map<String,String> keyMap;
360
361   private final Set<String> rbKeys;
362
363
364   /**
365    * Constructor.
366    *
367    * @param builder
368    *    The builder for this object.
369    */
370   protected Messages(Builder builder) {
371      this(builder.forClass, builder.getBundle(), builder.locale, builder.parent);
372   }
373
374   Messages(Class<?> forClass, ResourceBundle rb, Locale locale, Messages parent) {
375      this.c = forClass;
376      this.rb = rb;
377      this.parent = parent;
378      if (parent != null)
379         setParent(parent);
380      this.locale = locale == null ? Locale.getDefault() : locale;
381
382      Map<String,String> keyMap = new TreeMap<>();
383
384      String cn = c.getSimpleName() + '.';
385      if (rb != null) {
386         rb.keySet().forEach(x -> {
387            keyMap.put(x, x);
388            if (x.startsWith(cn)) {
389               String shortKey = x.substring(cn.length());
390               keyMap.put(shortKey, x);
391            }
392         });
393      }
394      if (parent != null) {
395         parent.keySet().forEach(x -> {
396            keyMap.put(x, x);
397            if (x.startsWith(cn)) {
398               String shortKey = x.substring(cn.length());
399               keyMap.put(shortKey, x);
400            }
401         });
402      }
403
404      this.keyMap = u(copyOf(keyMap));
405      this.rbKeys = rb == null ? Collections.emptySet() : rb.keySet();
406   }
407
408   /**
409    * Returns this message bundle for the specified locale.
410    *
411    * @param locale The locale to get the messages for.
412    * @return A new {@link Messages} object.  Never <jk>null</jk>.
413    */
414   public Messages forLocale(Locale locale) {
415      if (locale == null)
416         locale = Locale.getDefault();
417      if (this.locale.equals(locale))
418         return this;
419      Messages mb = localizedMessages.get(locale);
420      if (mb == null) {
421         Messages parent = this.parent == null ? null : this.parent.forLocale(locale);
422         ResourceBundle rb = this.rb == null ? null : findBundle(this.rb.getBaseBundleName(), locale, c.getClassLoader());
423         mb = new Messages(c, rb, locale, parent);
424         localizedMessages.put(locale, mb);
425      }
426      return mb;
427   }
428
429   /**
430    * Returns all keys in this resource bundle with the specified prefix.
431    *
432    * <p>
433    * Keys are returned in alphabetical order.
434    *
435    * @param prefix The prefix.
436    * @return The set of all keys in the resource bundle with the prefix.
437    */
438   public Set<String> keySet(String prefix) {
439      Set<String> set = Utils.set();
440      keySet().forEach(x -> {
441         if (x.equals(prefix) || (x.startsWith(prefix) && x.charAt(prefix.length()) == '.'))
442            set.add(x);
443      });
444      return set;
445   }
446
447   /**
448    * Similar to {@link ResourceBundle#getString(String)} except allows you to pass in {@link MessageFormat} objects.
449    *
450    * @param key The resource bundle key.
451    * @param args Optional {@link MessageFormat}-style arguments.
452    * @return
453    *    The resolved value.  Never <jk>null</jk>.
454    *    <js>"{!key}"</js> if the key is missing.
455    */
456   public String getString(String key, Object...args) {
457      String s = getString(key);
458      if (s.startsWith("{!"))
459         return s;
460      return format(s, args);
461   }
462
463   /**
464    * Looks for all the specified keys in the resource bundle and returns the first value that exists.
465    *
466    * @param keys The list of possible keys.
467    * @return The resolved value, or <jk>null</jk> if no value is found or the resource bundle is missing.
468    */
469   public String findFirstString(String...keys) {
470      for (String k : keys) {
471         if (containsKey(k))
472            return getString(k);
473      }
474      return null;
475   }
476
477   @Override /* ResourceBundle */
478   protected Object handleGetObject(String key) {
479      String k = keyMap.get(key);
480      if (k == null)
481         return "{!" + key + "}";
482      try {
483         if (rbKeys.contains(k))
484            return rb.getObject(k);
485      } catch (MissingResourceException e) { /* Shouldn't happen */ }
486      return parent.handleGetObject(key);
487   }
488
489   @Override /* ResourceBundle */
490   public boolean containsKey(String key) {
491      return keyMap.containsKey(key);
492   }
493
494   @Override /* ResourceBundle */
495   public Set<String> keySet() {
496      return keyMap.keySet();
497   }
498
499   @Override /* ResourceBundle */
500   public Enumeration<String> getKeys() {
501      return Collections.enumeration(keySet());
502   }
503
504   @Override /* Object */
505   public String toString() {
506      JsonMap m = new JsonMap();
507      for (String k : new TreeSet<>(keySet()))
508         m.put(k, getString(k));
509      return Json5.of(m);
510   }
511}