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.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.ResourceBundleUtils.*;
021import static org.apache.juneau.commons.utils.StringUtils.*;
022import static org.apache.juneau.commons.utils.ThrowableUtils.*;
023import static org.apache.juneau.commons.utils.Utils.*;
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.commons.collections.*;
032import org.apache.juneau.commons.function.*;
033import org.apache.juneau.commons.utils.*;
034import org.apache.juneau.marshaller.*;
035import org.apache.juneau.parser.ParseException;
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 */
114public class Messages extends ResourceBundle {
115   /**
116    * Builder class.
117    */
118   public static class Builder extends BeanBuilder<Messages> {
119
120      private static class MessagesString {
121         public String name;
122         public String[] baseNames;
123         public String locale;
124      }
125
126      Class<?> forClass;
127      Locale locale;
128      String name;
129      Messages parent;
130
131      List<Tuple2<Class<?>,String>> locations;
132
133      private String[] baseNames = { "{package}.{name}", "{package}.i18n.{name}", "{package}.nls.{name}", "{package}.messages.{name}" };
134
135      /**
136       * Constructor.
137       *
138       * @param forClass The base class.
139       */
140      protected Builder(Class<?> forClass) {
141         super(Messages.class, BeanStore.INSTANCE);
142         this.forClass = forClass;
143         this.name = cns(forClass);
144         locations = list();
145         locale = Locale.getDefault();
146      }
147
148      /**
149       * Specifies the base name patterns to use for finding the resource bundle.
150       *
151       * @param baseNames
152       *    The bundle base names.
153       *    <br>The default is the following:
154       *    <ul>
155       *       <li><js>"{package}.{name}"</js>
156       *       <li><js>"{package}.i18n.{name}"</js>
157       *       <li><js>"{package}.nls.{name}"</js>
158       *       <li><js>"{package}.messages.{name}"</js>
159       *    </ul>
160       * @return This object.
161       */
162      public Builder baseNames(String...baseNames) {
163         this.baseNames = baseNames == null ? a() : baseNames;
164         return this;
165      }
166
167      @Override /* Overridden from BeanBuilder */
168      public Builder impl(Object value) {
169         super.impl(value);
170         return this;
171      }
172
173      /**
174       * Specifies the locale.
175       *
176       * @param value
177       *    The locale.
178       *    If <jk>null</jk>, the default locale is used.
179       * @return This object.
180       */
181      public Builder locale(Locale value) {
182         this.locale = value == null ? Locale.getDefault() : value;
183         return this;
184      }
185
186      /**
187       * Specifies the locale.
188       *
189       * @param value
190       *    The locale.
191       *    If <jk>null</jk>, the default locale is used.
192       * @return This object.
193       */
194      public Builder locale(String value) {
195         return locale(value == null ? null : Locale.forLanguageTag(value));
196      }
197
198      /**
199       * Specifies a location of where to look for messages.
200       *
201       * @param baseClass The base class.
202       * @param bundlePath The bundle path.
203       * @return This object.
204       */
205      public Builder location(Class<?> baseClass, String bundlePath) {
206         this.locations.add(0, Tuple2.of(baseClass, bundlePath));
207         return this;
208      }
209
210      /**
211       * Specifies a location of where to look for messages.
212       *
213       * @param bundlePath The bundle path.
214       * @return This object.
215       */
216      public Builder location(String bundlePath) {
217         this.locations.add(0, Tuple2.of(forClass, bundlePath));
218         return this;
219      }
220
221      /**
222       * Specifies the bundle name (e.g. <js>"Messages"</js>).
223       *
224       * @param value
225       *    The bundle name.
226       *    <br>If <jk>null</jk>, the forClass class name is used.
227       * @return This object.
228       */
229      public Builder name(String value) {
230         this.name = isEmpty(value) ? cns(forClass) : value;
231         return this;
232      }
233
234      /**
235       * Adds a parent bundle.
236       *
237       * @param value The parent bundle.  Can be <jk>null</jk>.
238       * @return This object.
239       */
240      public Builder parent(Messages value) {
241         parent = value;
242         return this;
243      }
244
245      @Override /* Overridden from BeanBuilder */
246      public Builder type(Class<?> value) {
247         super.type(value);
248         return this;
249      }
250
251      @SuppressWarnings("unchecked")
252      @Override /* Overridden from BeanBuilder */
253      protected Messages buildDefault() {
254
255         if (! locations.isEmpty()) {
256            Tuple2<Class<?>,String>[] mbl = locations.toArray(new Tuple2[0]);
257
258            var x = (Builder)null;
259
260            for (var i = mbl.length - 1; i >= 0; i--) {
261               var c = firstNonNull(mbl[i].getA(), forClass);
262               var value = mbl[i].getB();
263               if (isProbablyJsonObject(value, true)) {
264                  MessagesString ms;
265                  try {
266                     ms = Json5.DEFAULT.read(value, MessagesString.class);
267                  } catch (ParseException e) {
268                     throw toRex(e);
269                  }
270                  x = Messages.create(c).name(ms.name).baseNames(StringUtils.splita(ms.baseNames, ',')).locale(ms.locale).parent(x == null ? null : x.build());
271               } else {
272                  x = Messages.create(c).name(value).parent(x == null ? null : x.build());
273               }
274            }
275
276            return x == null ? null : x.build();  // Shouldn't be null.
277         }
278
279         return new Messages(this);
280      }
281
282      ResourceBundle getBundle() {
283         var cl = forClass.getClassLoader();
284         var m = JsonMap.of("name", name, "package", forClass.getPackage().getName());
285         for (var bn : baseNames) {
286            bn = StringUtils.formatNamed(bn, m);
287            var rb = findBundle(bn, locale, cl);
288            if (nn(rb))
289               return rb;
290         }
291         return null;
292      }
293   }
294
295   /**
296    * Static creator.
297    *
298    * @param forClass
299    *    The class we're creating this object for.
300    * @return A new builder.
301    */
302   public static final Builder create(Class<?> forClass) {
303      return new Builder(forClass);
304   }
305
306   /**
307    * Constructor.
308    *
309    * @param forClass
310    *    The class we're creating this object for.
311    * @return A new message bundle belonging to the class.
312    */
313   public static final Messages of(Class<?> forClass) {
314      return create(forClass).build();
315   }
316
317   /**
318    * Constructor.
319    *
320    * @param forClass
321    *    The class we're creating this object for.
322    * @param name
323    *    The bundle name (e.g. <js>"Messages"</js>).
324    *    <br>If <jk>null</jk>, uses the class name.
325    * @return A new message bundle belonging to the class.
326    */
327   public static final Messages of(Class<?> forClass, String name) {
328      return create(forClass).name(name).build();
329   }
330
331   private ResourceBundle rb;
332   private Class<?> c;
333   private Messages parent;
334   private Locale locale;
335
336   // Cache of message bundles per locale.
337   private final ConcurrentHashMap<Locale,Messages> localizedMessages = new ConcurrentHashMap<>();
338
339   // Cache of virtual keys to actual keys.
340   private final Map<String,String> keyMap;
341
342   private final Set<String> rbKeys;
343
344   /**
345    * Constructor.
346    *
347    * @param builder
348    *    The builder for this object.
349    */
350   protected Messages(Builder builder) {
351      this(builder.forClass, builder.getBundle(), builder.locale, builder.parent);
352   }
353
354   Messages(Class<?> forClass, ResourceBundle rb, Locale locale, Messages parent) {
355      this.c = forClass;
356      this.rb = rb;
357      this.parent = parent;
358      if (nn(parent))
359         setParent(parent);
360      this.locale = locale == null ? Locale.getDefault() : locale;
361
362      var keyMap = new TreeMap<String,String>();
363
364      var cn = cns(c) + '.';
365      if (nn(rb)) {
366         rb.keySet().forEach(x -> {
367            keyMap.put(x, x);
368            if (x.startsWith(cn)) {
369               var shortKey = x.substring(cn.length());
370               keyMap.put(shortKey, x);
371            }
372         });
373      }
374      if (nn(parent)) {
375         parent.keySet().forEach(x -> {
376            keyMap.put(x, x);
377            if (x.startsWith(cn)) {
378               var shortKey = x.substring(cn.length());
379               keyMap.put(shortKey, x);
380            }
381         });
382      }
383
384      this.keyMap = u(copyOf(keyMap));
385      this.rbKeys = rb == null ? Collections.emptySet() : rb.keySet();
386   }
387
388   @Override /* Overridden from ResourceBundle */
389   public boolean containsKey(String key) {
390      return keyMap.containsKey(key);
391   }
392
393   /**
394    * Looks for all the specified keys in the resource bundle and returns the first value that exists.
395    *
396    * @param keys The list of possible keys.
397    * @return The resolved value, or <jk>null</jk> if no value is found or the resource bundle is missing.
398    */
399   public String findFirstString(String...keys) {
400      for (var k : keys) {
401         if (containsKey(k))
402            return getString(k);
403      }
404      return null;
405   }
406
407   /**
408    * Returns this message bundle for the specified locale.
409    *
410    * @param locale The locale to get the messages for.
411    * @return A new {@link Messages} object.  Never <jk>null</jk>.
412    */
413   public Messages forLocale(Locale locale) {
414      if (locale == null)
415         locale = Locale.getDefault();
416      if (this.locale.equals(locale))
417         return this;
418      var mb = localizedMessages.get(locale);
419      if (mb == null) {
420         var parent = this.parent == null ? null : this.parent.forLocale(locale);
421         var rb = this.rb == null ? null : findBundle(this.rb.getBaseBundleName(), locale, c.getClassLoader());
422         mb = new Messages(c, rb, locale, parent);
423         localizedMessages.put(locale, mb);
424      }
425      return mb;
426   }
427
428   @Override /* Overridden from ResourceBundle */
429   public Enumeration<String> getKeys() { return Collections.enumeration(keySet()); }
430
431   /**
432    * Similar to {@link ResourceBundle#getString(String)} except allows you to pass in {@link MessageFormat} objects.
433    *
434    * @param key The resource bundle key.
435    * @param args Optional {@link MessageFormat}-style arguments.
436    * @return
437    *    The resolved value.  Never <jk>null</jk>.
438    *    <js>"{!key}"</js> if the key is missing.
439    */
440   public String getString(String key, Object...args) {
441      var s = getString(key);
442      if (s.startsWith("{!"))
443         return s;
444      return f(s, args);
445   }
446
447   @Override /* Overridden from ResourceBundle */
448   public Set<String> keySet() {
449      return keyMap.keySet();
450   }
451
452   /**
453    * Returns all keys in this resource bundle with the specified prefix.
454    *
455    * <p>
456    * Keys are returned in alphabetical order.
457    *
458    * @param prefix The prefix.
459    * @return The set of all keys in the resource bundle with the prefix.
460    */
461   public Set<String> keySet(String prefix) {
462      Set<String> set = set();
463      keySet().forEach(x -> {
464         if (x.equals(prefix) || (x.startsWith(prefix) && x.charAt(prefix.length()) == '.'))
465            set.add(x);
466      });
467      return set;
468   }
469
470   protected FluentMap<String,Object> properties() {
471      // @formatter:off
472      var m = filteredBeanPropertyMap();
473      keySet().stream().forEach(x -> m.a(x, getString(x)));
474      return m;
475      // @formatter:on
476   }
477
478   @Override /* Overridden from Object */
479   public String toString() {
480      return r(properties());
481   }
482
483   @Override /* Overridden from ResourceBundle */
484   protected Object handleGetObject(String key) {
485      var k = keyMap.get(key);
486      if (k == null)
487         return "{!" + key + "}";
488      try {
489         if (rbKeys.contains(k))
490            return rb.getObject(k);
491      } catch (@SuppressWarnings("unused") MissingResourceException e) { /* Shouldn't happen */ }
492      return parent.handleGetObject(key);
493   }
494}