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.internal.ResourceBundleUtils.*;
016import static org.apache.juneau.internal.StringUtils.*;
017
018import java.text.*;
019import java.util.*;
020import java.util.concurrent.*;
021
022import org.apache.juneau.collections.*;
023import org.apache.juneau.marshall.*;
024
025/**
026 * An enhanced {@link ResourceBundle}.
027 *
028 * <p>
029 * Wraps a ResourceBundle to provide some useful additional functionality.
030 *
031 * <ul>
032 *    <li>
033 *       Instead of throwing {@link MissingResourceException}, the {@link ResourceBundle#getString(String)} method
034 *       will return <js>"{!key}"</js> if the message could not be found.
035 *    <li>
036 *       Supported hierarchical lookup of resources from parent parent classes.
037 *    <li>
038 *       Support for easy retrieval of localized bundles.
039 *    <li>
040 *       Support for generalized resource bundles (e.g. properties files containing keys for several classes).
041 * </ul>
042 *
043 * <p>
044 * The following example shows the basic usage of this class for retrieving localized messages:
045 *
046 * <p class='bcode w800'>
047 *    <cc># Contents of MyClass.properties</cc>
048 *    <ck>foo</ck> = <cv>foo {0}</cv>
049 *    <ck>MyClass.bar</ck> = <cv>bar {0}</cv>
050 * </p>
051 * <p class='bcode w800'>
052 *    <jk>public class</jk> MyClass {
053 *       <jk>private static final</jk> Messages <jsf>MESSAGES</jsf> = Messages.<jsm>of</jsm>(MyClass.<jk>class</jk>);
054 *
055 *       <jk>public void</jk> doFoo() {
056 *
057 *       <jc>// A normal property.</jc>
058 *          String <jv>foo</jv> = <jsf>MESSAGES</jsf>.getString(<js>"foo"</js>,<js>"x"</js>);  <jc>// == "foo x"</jc>
059 *
060 *          <jc>// A property prefixed by class name.</jc>
061 *          String <jv>bar</jv> = <jsf>MESSAGES</jsf>.getString(<js>"bar"</js>,<js>"x"</js>);  <jc>// == "bar x"</jc>
062 *
063 *          <jc>// A non-existent property.</jc>
064 *          String <jv>baz</jv> = <jsf>MESSAGES</jsf>.getString(<js>"baz"</js>,<js>"x"</js>);  <jc>// == "{!baz}"</jc>
065 *       }
066 *    }
067 * </p>
068 *
069 * <p>
070 *    The ability to resolve keys prefixed by class name allows you to place all your messages in a single file such
071 *    as a common <js>"Messages.properties"</js> file along with those for other classes.
072 * <p>
073 *    The following shows how to retrieve messages from a common bundle:
074 *
075 * <p class='bcode w800'>
076 *    <jk>public class</jk> MyClass {
077 *       <jk>private static final</jk> Messages <jsf>MESSAGES</jsf> = Messages.<jsm>of</jsm>(MyClass.<jk>class</jk>, <js>"Messages"</js>);
078 *    }
079 * </p>
080 *
081 * <p>
082 *    Resource bundles are searched using the following base name patterns:
083 *    <ul>
084 *       <li><js>"{package}.{name}"</js>
085 *       <li><js>"{package}.i18n.{name}"</js>
086 *       <li><js>"{package}.nls.{name}"</js>
087 *       <li><js>"{package}.messages.{name}"</js>
088 *    </ul>
089 *
090 * <p>
091 *    These patterns can be customized using the {@link MessagesBuilder#baseNames(String...)} method.
092 *
093 * <p>
094 *    Localized messages can be retrieved in the following way:
095 *
096* <p class='bcode w800'>
097*  <jc>// Return value from Japan locale bundle.</jc>
098*  String <jv>foo</jv> = <jsf>MESSAGES</jsf>.forLocale(Locale.<jsf>JAPAN</jsf>).getString(<js>"foo"</js>);
099 * </p>
100 */
101public class Messages extends ResourceBundle {
102
103   private ResourceBundle rb;
104   private Class<?> c;
105   private Messages parent;
106   private Locale locale;
107
108   // Cache of message bundles per locale.
109   private final ConcurrentHashMap<Locale,Messages> localizedMessages = new ConcurrentHashMap<>();
110
111   // Cache of virtual keys to actual keys.
112   private final Map<String,String> keyMap;
113
114   private final Set<String> rbKeys;
115
116   /**
117    * Creator.
118    *
119    * @param forClass
120    *    The class we're creating this object for.
121    * @return A new builder.
122    */
123   public static final MessagesBuilder create(Class<?> forClass) {
124      return new MessagesBuilder(forClass);
125   }
126
127   /**
128    * Constructor.
129    *
130    * @param forClass
131    *    The class we're creating this object for.
132    * @return A new message bundle belonging to the class.
133    */
134   public static final Messages of(Class<?> forClass) {
135      return create(forClass).build();
136   }
137
138   /**
139    * Constructor.
140    *
141    * @param forClass
142    *    The class we're creating this object for.
143    * @param name
144    *    The bundle name (e.g. <js>"Messages"</js>).
145    *    <br>If <jk>null</jk>, uses the class name.
146    * @return A new message bundle belonging to the class.
147    */
148   public static final Messages of(Class<?> forClass, String name) {
149      return create(forClass).name(name).build();
150   }
151
152
153   /**
154    * Constructor.
155    *
156    * @param forClass
157    *    The class we're creating this object for.
158    * @param rb
159    *    The resource bundle we're encapsulating.  Can be <jk>null</jk>.
160    * @param locale The locale of these messages.
161    * @param parent
162    *    The parent resource.  Can be <jk>null</jk>.
163    */
164   public Messages(Class<?> forClass, ResourceBundle rb, Locale locale, Messages parent) {
165      this.c = forClass;
166      this.rb = rb;
167      this.parent = parent;
168      if (parent != null)
169         setParent(parent);
170      this.locale = locale == null ? Locale.getDefault() : locale;
171
172      Map<String,String> keyMap = new TreeMap<>();
173
174      String cn = c.getSimpleName() + '.';
175      if (rb != null) {
176         for (String key : rb.keySet()) {
177            keyMap.put(key, key);
178            if (key.startsWith(cn)) {
179               String shortKey = key.substring(cn.length());
180               keyMap.put(shortKey, key);
181            }
182         }
183      }
184      if (parent != null) {
185         for (String key : parent.keySet()) {
186            keyMap.put(key, key);
187            if (key.startsWith(cn)) {
188               String shortKey = key.substring(cn.length());
189               keyMap.put(shortKey, key);
190            }
191         }
192      }
193
194      this.keyMap = Collections.unmodifiableMap(new LinkedHashMap<>(keyMap));
195      this.rbKeys = rb == null ? Collections.emptySet() : rb.keySet();
196   }
197
198   /**
199    * Returns this message bundle for the specified locale.
200    *
201    * @param locale The locale to get the messages for.
202    * @return A new {@link Messages} object.  Never <jk>null</jk>.
203    */
204   public Messages forLocale(Locale locale) {
205      if (locale == null)
206         locale = Locale.getDefault();
207      if (this.locale.equals(locale))
208         return this;
209      Messages mb = localizedMessages.get(locale);
210      if (mb == null) {
211         Messages parent = this.parent == null ? null : this.parent.forLocale(locale);
212         ResourceBundle rb = this.rb == null ? null : findBundle(this.rb.getBaseBundleName(), locale, c.getClassLoader());
213         mb = new Messages(c, rb, locale, parent);
214         localizedMessages.put(locale, mb);
215      }
216      return mb;
217   }
218
219   /**
220    * Returns all keys in this resource bundle with the specified prefix.
221    *
222    * <p>
223    * Keys are returned in alphabetical order.
224    *
225    * @param prefix The prefix.
226    * @return The set of all keys in the resource bundle with the prefix.
227    */
228   public Set<String> keySet(String prefix) {
229      Set<String> set = new LinkedHashSet<>();
230      for (String s : keySet()) {
231         if (s.equals(prefix) || (s.startsWith(prefix) && s.charAt(prefix.length()) == '.'))
232            set.add(s);
233      }
234      return set;
235   }
236
237   /**
238    * Similar to {@link ResourceBundle#getString(String)} except allows you to pass in {@link MessageFormat} objects.
239    *
240    * @param key The resource bundle key.
241    * @param args Optional {@link MessageFormat}-style arguments.
242    * @return
243    *    The resolved value.  Never <jk>null</jk>.
244    *    <js>"{!key}"</js> if the key is missing.
245    */
246   public String getString(String key, Object...args) {
247      String s = getString(key);
248      if (s.startsWith("{!"))
249         return s;
250      return format(s, args);
251   }
252
253   /**
254    * Looks for all the specified keys in the resource bundle and returns the first value that exists.
255    *
256    * @param keys The list of possible keys.
257    * @return The resolved value, or <jk>null</jk> if no value is found or the resource bundle is missing.
258    */
259   public String findFirstString(String...keys) {
260      for (String k : keys) {
261         if (containsKey(k))
262            return getString(k);
263      }
264      return null;
265   }
266
267   @Override /* ResourceBundle */
268   protected Object handleGetObject(String key) {
269      String k = keyMap.get(key);
270      if (k == null)
271         return "{!" + key + "}";
272      try {
273         if (rbKeys.contains(k))
274            return rb.getObject(k);
275      } catch (MissingResourceException e) { /* Shouldn't happen */ }
276      return parent.handleGetObject(key);
277   }
278
279   @Override /* ResourceBundle */
280   public boolean containsKey(String key) {
281      return keyMap.containsKey(key);
282   }
283
284   @Override /* ResourceBundle */
285   public Set<String> keySet() {
286      return keyMap.keySet();
287   }
288
289   @Override /* ResourceBundle */
290   public Enumeration<String> getKeys() {
291      return Collections.enumeration(keySet());
292   }
293
294   @Override
295   public String toString() {
296      OMap om = new OMap();
297      for (String k : new TreeSet<>(keySet()))
298         om.put(k, getString(k));
299      return SimpleJson.DEFAULT.toString(om);
300   }
301}