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