blob: 65978252c636afe8a798c89cc7668a099f0f943e [file] [log] [blame]
// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
// * with the License. You may obtain a copy of the License at *
// * *
// * http://www.apache.org/licenses/LICENSE-2.0 *
// * *
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
// * specific language governing permissions and limitations under the License. *
// ***************************************************************************************************************************
package org.apache.juneau.utils;
import static org.apache.juneau.internal.StringUtils.*;
import static org.apache.juneau.internal.ThrowableUtils.*;
import java.text.*;
import java.util.*;
import java.util.concurrent.*;
import org.apache.juneau.*;
/**
* Wraps a {@link ResourceBundle} to provide some useful additional functionality.
*
* <ul class='spaced-list'>
* <li>
* Instead of throwing {@link MissingResourceException}, the {@link #getString(String)} method
* will return <js>"{!!key}"</js> if the bundle was not found, and <js>"{!key}"</js> if bundle
* was found but the key is not in the bundle.
* <li>
* A client locale can be set as a {@link ThreadLocal} object using the static {@link #setClientLocale(Locale)}
* so that client localized messages can be retrieved using the {@link #getClientString(String, Object...)}
* method on all instances of this class.
* <li>
* Resource bundles on parent classes can be added to the search path for this class by using the
* {@link #addSearchPath(Class, String)} method.
* This allows messages to be retrieved from the resource bundles of parent classes.
* <li>
* Locale-specific bundles can be retrieved by using the {@link #getBundle(Locale)} method.
* <li>
* The {@link #getString(Locale, String, Object...)} method can be used to retrieve locale-specific messages.
* <li>
* Messages in the resource bundle can optionally be prefixed with the simple class name.
* For example, if the class is <c>MyClass</c> and the properties file contains <js>"MyClass.myMessage"</js>,
* the message can be retrieved using <code>getString(<js>"myMessage"</js>)</code>.
* </ul>
*
* <ul class='notes'>
* <li>
* This class is thread-safe.
* </ul>
*/
public class MessageBundle extends ResourceBundle {
private static final ThreadLocal<Locale> clientLocale = new ThreadLocal<>();
private final ResourceBundle rb;
private final String bundlePath, className;
private final Class<?> forClass;
private final long creationThreadId;
// A map that contains all keys [shortKeyName->keyName] and [keyName->keyName], where shortKeyName
// refers to keys prefixed and stripped of the class name (e.g. "foobar"->"MyClass.foobar")
private final Map<String,String> keyMap = new ConcurrentHashMap<>();
// Contains all keys present in all bundles in searchBundles.
private final ConcurrentSkipListSet<String> allKeys = new ConcurrentSkipListSet<>();
// Bundles to search through to find properties.
// Typically this will be a list of resource bundles for each class up the class hierarchy chain.
private final CopyOnWriteArrayList<MessageBundle> searchBundles = new CopyOnWriteArrayList<>();
// Cache of message bundles per locale.
private final ConcurrentHashMap<Locale,MessageBundle> localizedBundles = new ConcurrentHashMap<>();
/**
* Sets the locale for this thread so that calls to {@link #getClientString(String, Object...)} return messages in
* that locale.
*
* @param locale The new client locale.
*/
public static void setClientLocale(Locale locale) {
MessageBundle.clientLocale.set(locale);
}
/**
* Constructor.
*
* <p>
* When this method is used, the bundle path is determined by searching for the resource bundle
* in the following locations:
* <ul>
* <li><c>[package].ForClass.properties</c>
* <li><c>[package].nls.ForClass.properties</c>
* <li><c>[package].i18n.ForClass.properties</c>
* </ul>
*
* @param forClass The class
* @return A new message bundle belonging to the class.
*/
public static final MessageBundle create(Class<?> forClass) {
return create(forClass, findBundlePath(forClass));
}
/**
* Constructor.
*
* <p>
* A shortcut for calling <c>new MessageBundle(forClass, bundlePath)</c>.
*
* @param forClass The class
* @param bundlePath The location of the resource bundle.
* @return A new message bundle belonging to the class.
*/
public static final MessageBundle create(Class<?> forClass, String bundlePath) {
return new MessageBundle(forClass, bundlePath);
}
private static final String findBundlePath(Class<?> forClass) {
String path = forClass.getName();
if (tryBundlePath(forClass, path))
return path;
path = forClass.getPackage().getName() + ".nls." + forClass.getSimpleName();
if (tryBundlePath(forClass, path))
return path;
path = forClass.getPackage().getName() + ".i18n." + forClass.getSimpleName();
if (tryBundlePath(forClass, path))
return path;
return null;
}
private static final boolean tryBundlePath(Class<?> c, String path) {
try {
path = c.getName();
ResourceBundle.getBundle(path, Locale.getDefault(), c.getClassLoader());
return true;
} catch (MissingResourceException e) {
return false;
}
}
/**
* Constructor.
*
* @param forClass The class using this resource bundle.
* @param bundlePath
* The path of the resource bundle to wrap.
* This can be an absolute path (e.g. <js>"com.foo.MyMessages"</js>) or a path relative to the package of the
* <l>forClass</l> (e.g. <js>"MyMessages"</js> if <l>forClass</l> is <js>"com.foo.MyClass"</js>).
*/
public MessageBundle(Class<?> forClass, String bundlePath) {
this(forClass, bundlePath, Locale.getDefault());
}
private MessageBundle(Class<?> forClass, String bundlePath, Locale locale) {
this.forClass = forClass;
this.className = forClass.getSimpleName();
if (bundlePath == null)
throw new RuntimeException("Bundle path was null.");
if (bundlePath.endsWith(".properties"))
throw new RuntimeException("Bundle path should not end with '.properties'");
this.bundlePath = bundlePath;
this.creationThreadId = Thread.currentThread().getId();
ClassLoader cl = forClass.getClassLoader();
ResourceBundle trb = null;
try {
trb = ResourceBundle.getBundle(bundlePath, locale, cl);
} catch (MissingResourceException e) {
try {
trb = ResourceBundle.getBundle(forClass.getPackage().getName() + '.' + bundlePath, locale, cl);
} catch (MissingResourceException e2) {
}
}
this.rb = trb;
if (rb != null) {
// Populate keyMap with original mappings.
for (Enumeration<String> e = getKeys(); e.hasMoreElements();) {
String key = e.nextElement();
keyMap.put(key, key);
}
// Override/augment with shortname mappings (e.g. "foobar"->"MyClass.foobar")
String c = className + '.';
for (Enumeration<String> e = getKeys(); e.hasMoreElements();) {
String key = e.nextElement();
if (key.startsWith(c)) {
String shortKey = key.substring(className.length() + 1);
keyMap.put(shortKey, key);
}
}
allKeys.addAll(keyMap.keySet());
}
searchBundles.add(this);
}
/**
* Add another bundle path to this resource bundle.
*
* <p>
* Order of property lookup is first-to-last.
*
* <p>
* This method must be called from the same thread as the call to the constructor.
* This eliminates the need for synchronization.
*
* @param forClass The class using this resource bundle.
* @param bundlePath The bundle path.
* @return This object (for method chaining).
*/
public MessageBundle addSearchPath(Class<?> forClass, String bundlePath) {
assertSameThread(creationThreadId, "This method can only be called from the same thread that created the object.");
MessageBundle srb = new MessageBundle(forClass, bundlePath);
if (srb.rb != null) {
allKeys.addAll(srb.keySet());
searchBundles.add(srb);
}
return this;
}
@Override /* ResourceBundle */
public boolean containsKey(String key) {
return allKeys.contains(key);
}
/**
* Similar to {@link ResourceBundle#getString(String)} except allows you to pass in {@link MessageFormat} objects.
*
* @param key The resource bundle key.
* @param args Optional {@link MessageFormat}-style arguments.
* @return
* The resolved value. Never <jk>null</jk>.
* <js>"{!!key}"</js> if the bundle is missing.
* <js>"{!key}"</js> if the key is missing.
*/
public String getString(String key, Object...args) {
String s = getString(key);
if (s.length() > 0 && s.charAt(0) == '{')
return s;
return format(s, args);
}
/**
* Same as {@link #getString(String, Object...)} but allows you to specify the locale.
*
* @param locale The locale of the resource bundle to retrieve message from.
* @param key The resource bundle key.
* @param args Optional {@link MessageFormat}-style arguments.
* @return
* The resolved value. Never <jk>null</jk>.
* <js>"{!!key}"</js> if the bundle is missing.
* <js>"{!key}"</js> if the key is missing.
*/
public String getString(Locale locale, String key, Object...args) {
if (locale == null)
return getString(key, args);
return getBundle(locale).getString(key, args);
}
/**
* Same as {@link #getString(String, Object...)} but uses the locale specified on the call to {@link #setClientLocale(Locale)}.
*
* @param key The resource bundle key.
* @param args Optional {@link MessageFormat}-style arguments.
* @return
* The resolved value. Never <jk>null</jk>.
* <js>"{!!key}"</js> if the bundle is missing.
* <js>"{!key}"</js> if the key is missing.
*/
public String getClientString(String key, Object...args) {
return getString(clientLocale.get(), key, args);
}
/**
* Looks for all the specified keys in the resource bundle and returns the first value that exists.
*
* @param keys The list of possible keys.
* @return The resolved value, or <jk>null</jk> if no value is found or the resource bundle is missing.
*/
public String findFirstString(String...keys) {
if (rb == null)
return null;
for (String k : keys) {
if (containsKey(k))
return getString(k);
}
return null;
}
/**
* Same as {@link #findFirstString(String...)}, but uses the specified locale.
*
* @param locale The locale of the resource bundle to retrieve message from.
* @param keys The list of possible keys.
* @return The resolved value, or <jk>null</jk> if no value is found or the resource bundle is missing.
*/
public String findFirstString(Locale locale, String...keys) {
MessageBundle srb = getBundle(locale);
return srb.findFirstString(keys);
}
@Override /* ResourceBundle */
public Set<String> keySet() {
return Collections.unmodifiableSet(allKeys);
}
/**
* Returns all keys in this resource bundle with the specified prefix.
*
* @param prefix The prefix.
* @return The set of all keys in the resource bundle with the prefix.
*/
public Set<String> keySet(String prefix) {
Set<String> set = new HashSet<>();
for (String s : keySet()) {
if (s.equals(prefix) || (s.startsWith(prefix) && s.charAt(prefix.length()) == '.'))
set.add(s);
}
return set;
}
@Override /* ResourceBundle */
public Enumeration<String> getKeys() {
if (rb == null)
return new Vector<String>(0).elements();
return rb.getKeys();
}
@Override /* ResourceBundle */
protected Object handleGetObject(String key) {
for (MessageBundle srb : searchBundles) {
if (srb.rb != null) {
String key2 = srb.keyMap.get(key);
if (key2 != null) {
try {
return srb.rb.getObject(key2);
} catch (Exception e) {
return "{!"+key+"}";
}
}
}
}
if (rb == null)
return "{!!"+key+"}";
return "{!"+key+"}";
}
/**
* Returns this resource bundle as an {@link ObjectMap}.
*
* <p>
* Useful for debugging purposes.
* Note that any class that implements a <c>swap()</c> method will automatically be serialized by
* calling this method and serializing the result.
*
* <p>
* This method always constructs a new {@link ObjectMap} on each call.
*
* @return A new map containing all the keys and values in this bundle.
*/
public ObjectMap swap() {
ObjectMap om = new ObjectMap();
for (String k : allKeys)
om.put(k, getString(k));
return om;
}
/**
* Returns the resource bundle for the specified locale.
*
* @param locale The client locale.
* @return The resource bundle for the specified locale. Never <jk>null</jk>.
*/
public MessageBundle getBundle(Locale locale) {
MessageBundle mb = localizedBundles.get(locale);
if (mb != null)
return mb;
mb = new MessageBundle(forClass, bundlePath, locale);
List<MessageBundle> l = new ArrayList<>(searchBundles.size()-1);
for (int i = 1; i < searchBundles.size(); i++) {
MessageBundle srb = searchBundles.get(i);
srb = new MessageBundle(srb.forClass, srb.bundlePath, locale);
l.add(srb);
mb.allKeys.addAll(srb.keySet());
}
mb.searchBundles.addAll(l);
localizedBundles.putIfAbsent(locale, mb);
return localizedBundles.get(locale);
}
}