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