org.apache.juneau.cp.Messages Maven / Gradle / Ivy
// ***************************************************************************************************************************
// * 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.cp;
import static org.apache.juneau.common.internal.StringUtils.*;
import static org.apache.juneau.common.internal.ThrowableUtils.*;
import static org.apache.juneau.internal.CollectionUtils.*;
import static org.apache.juneau.internal.ObjectUtils.*;
import static org.apache.juneau.internal.ResourceBundleUtils.*;
import java.text.*;
import java.util.*;
import java.util.concurrent.*;
import org.apache.juneau.*;
import org.apache.juneau.collections.*;
import org.apache.juneau.common.internal.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.marshaller.*;
import org.apache.juneau.parser.ParseException;
import org.apache.juneau.utils.*;
/**
* An enhanced {@link ResourceBundle}.
*
*
* Wraps a ResourceBundle to provide some useful additional functionality.
*
*
* -
* Instead of throwing {@link MissingResourceException}, the {@link ResourceBundle#getString(String)} method
* will return
"{!key}" if the message could not be found.
* -
* Supported hierarchical lookup of resources from parent parent classes.
*
-
* Support for easy retrieval of localized bundles.
*
-
* Support for generalized resource bundles (e.g. properties files containing keys for several classes).
*
*
*
* The following example shows the basic usage of this class for retrieving localized messages:
*
*
* # Contents of MyClass.properties
* foo = foo {0}
* MyClass.bar = bar {0}
*
*
* public class MyClass {
* private static final Messages MESSAGES = Messages.of (MyClass.class );
*
* public void doFoo() {
*
* // A normal property.
* String foo = MESSAGES .getString("foo" ,"x" ); // == "foo x"
*
* // A property prefixed by class name.
* String bar = MESSAGES .getString("bar" ,"x" ); // == "bar x"
*
* // A non-existent property.
* String baz = MESSAGES .getString("baz" ,"x" ); // == "{!baz}"
* }
* }
*
*
*
* The ability to resolve keys prefixed by class name allows you to place all your messages in a single file such
* as a common "Messages.properties" file along with those for other classes.
*
* The following shows how to retrieve messages from a common bundle:
*
*
* public class MyClass {
* private static final Messages MESSAGES = Messages.of (MyClass.class , "Messages" );
* }
*
*
*
* Resource bundles are searched using the following base name patterns:
*
* "{package}.{name}"
* "{package}.i18n.{name}"
* "{package}.nls.{name}"
* "{package}.messages.{name}"
*
*
*
* These patterns can be customized using the {@link Builder#baseNames(String...)} method.
*
*
* Localized messages can be retrieved in the following way:
*
*
* // Return value from Japan locale bundle.
* String foo = MESSAGES .forLocale(Locale.JAPAN ).getString("foo" );
*
*
* See Also:
*
*/
public class Messages extends ResourceBundle {
//-----------------------------------------------------------------------------------------------------------------
// Static
//-----------------------------------------------------------------------------------------------------------------
/**
* Static creator.
*
* @param forClass
* The class we're creating this object for.
* @return A new builder.
*/
public static final Builder create(Class> forClass) {
return new Builder(forClass);
}
/**
* Constructor.
*
* @param forClass
* The class we're creating this object for.
* @return A new message bundle belonging to the class.
*/
public static final Messages of(Class> forClass) {
return create(forClass).build();
}
/**
* Constructor.
*
* @param forClass
* The class we're creating this object for.
* @param name
* The bundle name (e.g. "Messages" ).
*
If null , uses the class name.
* @return A new message bundle belonging to the class.
*/
public static final Messages of(Class> forClass, String name) {
return create(forClass).name(name).build();
}
//-----------------------------------------------------------------------------------------------------------------
// Builder
//-----------------------------------------------------------------------------------------------------------------
/**
* Builder class.
*/
@FluentSetters
public static class Builder extends BeanBuilder {
Class> forClass;
Locale locale;
String name;
Messages parent;
List,String>> locations;
private String[] baseNames = {"{package}.{name}","{package}.i18n.{name}","{package}.nls.{name}","{package}.messages.{name}"};
/**
* Constructor.
*
* @param forClass The base class.
*/
protected Builder(Class> forClass) {
super(Messages.class, BeanStore.INSTANCE);
this.forClass = forClass;
this.name = forClass.getSimpleName();
locations = list();
locale = Locale.getDefault();
}
@Override /* BeanBuilder */
protected Messages buildDefault() {
if (! locations.isEmpty()) {
Tuple2,String>[] mbl = locations.toArray(new Tuple2[0]);
Builder x = null;
for (int i = mbl.length-1; i >= 0; i--) {
Class> c = firstNonNull(mbl[i].getA(), forClass);
String value = mbl[i].getB();
if (isJsonObject(value, true)) {
MessagesString ms;
try {
ms = Json5.DEFAULT.read(value, MessagesString.class);
} catch (ParseException e) {
throw asRuntimeException(e);
}
x = Messages.create(c).name(ms.name).baseNames(split(ms.baseNames, ',')).locale(ms.locale).parent(x == null ? null : x.build());
} else {
x = Messages.create(c).name(value).parent(x == null ? null : x.build());
}
}
return x == null ? null : x.build(); // Shouldn't be null.
}
return new Messages(this);
}
private static class MessagesString {
public String name;
public String[] baseNames;
public String locale;
}
//-------------------------------------------------------------------------------------------------------------
// Properties
//-------------------------------------------------------------------------------------------------------------
/**
* Adds a parent bundle.
*
* @param parent The parent bundle. Can be null .
* @return This object.
*/
public Builder parent(Messages parent) {
this.parent = parent;
return this;
}
/**
* Specifies the bundle name (e.g. "Messages" ).
*
* @param name
* The bundle name.
*
If null , the forClass class name is used.
* @return This object.
*/
public Builder name(String name) {
this.name = isEmpty(name) ? forClass.getSimpleName() : name;
return this;
}
/**
* Specifies the base name patterns to use for finding the resource bundle.
*
* @param baseNames
* The bundle base names.
*
The default is the following:
*
* "{package}.{name}"
* "{package}.i18n.{name}"
* "{package}.nls.{name}"
* "{package}.messages.{name}"
*
* @return This object.
*/
public Builder baseNames(String...baseNames) {
this.baseNames = baseNames == null ? new String[]{} : baseNames;
return this;
}
/**
* Specifies the locale.
*
* @param locale
* The locale.
* If null , the default locale is used.
* @return This object.
*/
public Builder locale(Locale locale) {
this.locale = locale == null ? Locale.getDefault() : locale;
return this;
}
/**
* Specifies the locale.
*
* @param locale
* The locale.
* If null , the default locale is used.
* @return This object.
*/
public Builder locale(String locale) {
return locale(locale == null ? null : Locale.forLanguageTag(locale));
}
/**
* Specifies a location of where to look for messages.
*
* @param baseClass The base class.
* @param bundlePath The bundle path.
* @return This object.
*/
public Builder location(Class> baseClass, String bundlePath) {
this.locations.add(0, Tuple2.of(baseClass, bundlePath));
return this;
}
/**
* Specifies a location of where to look for messages.
*
* @param bundlePath The bundle path.
* @return This object.
*/
public Builder location(String bundlePath) {
this.locations.add(0, Tuple2.of(forClass, bundlePath));
return this;
}
//
@Override /* GENERATED - org.apache.juneau.BeanBuilder */
public Builder impl(Object value) {
super.impl(value);
return this;
}
@Override /* GENERATED - org.apache.juneau.BeanBuilder */
public Builder type(Class> value) {
super.type(value);
return this;
}
//
//-------------------------------------------------------------------------------------------------------------
// Other methods
//-------------------------------------------------------------------------------------------------------------
ResourceBundle getBundle() {
ClassLoader cl = forClass.getClassLoader();
JsonMap m = JsonMap.of("name", name, "package", forClass.getPackage().getName());
for (String bn : baseNames) {
bn = StringUtils.replaceVars(bn, m);
ResourceBundle rb = findBundle(bn, locale, cl);
if (rb != null)
return rb;
}
return null;
}
}
//-----------------------------------------------------------------------------------------------------------------
// Instance
//-----------------------------------------------------------------------------------------------------------------
private ResourceBundle rb;
private Class> c;
private Messages parent;
private Locale locale;
// Cache of message bundles per locale.
private final ConcurrentHashMap localizedMessages = new ConcurrentHashMap<>();
// Cache of virtual keys to actual keys.
private final Map keyMap;
private final Set rbKeys;
/**
* Constructor.
*
* @param builder
* The builder for this object.
*/
protected Messages(Builder builder) {
this(builder.forClass, builder.getBundle(), builder.locale, builder.parent);
}
Messages(Class> forClass, ResourceBundle rb, Locale locale, Messages parent) {
this.c = forClass;
this.rb = rb;
this.parent = parent;
if (parent != null)
setParent(parent);
this.locale = locale == null ? Locale.getDefault() : locale;
Map keyMap = new TreeMap<>();
String cn = c.getSimpleName() + '.';
if (rb != null) {
rb.keySet().forEach(x -> {
keyMap.put(x, x);
if (x.startsWith(cn)) {
String shortKey = x.substring(cn.length());
keyMap.put(shortKey, x);
}
});
}
if (parent != null) {
parent.keySet().forEach(x -> {
keyMap.put(x, x);
if (x.startsWith(cn)) {
String shortKey = x.substring(cn.length());
keyMap.put(shortKey, x);
}
});
}
this.keyMap = unmodifiable(copyOf(keyMap));
this.rbKeys = rb == null ? Collections.emptySet() : rb.keySet();
}
/**
* Returns this message bundle for the specified locale.
*
* @param locale The locale to get the messages for.
* @return A new {@link Messages} object. Never null .
*/
public Messages forLocale(Locale locale) {
if (locale == null)
locale = Locale.getDefault();
if (this.locale.equals(locale))
return this;
Messages mb = localizedMessages.get(locale);
if (mb == null) {
Messages parent = this.parent == null ? null : this.parent.forLocale(locale);
ResourceBundle rb = this.rb == null ? null : findBundle(this.rb.getBaseBundleName(), locale, c.getClassLoader());
mb = new Messages(c, rb, locale, parent);
localizedMessages.put(locale, mb);
}
return mb;
}
/**
* Returns all keys in this resource bundle with the specified prefix.
*
*
* Keys are returned in alphabetical order.
*
* @param prefix The prefix.
* @return The set of all keys in the resource bundle with the prefix.
*/
public Set keySet(String prefix) {
Set set = set();
keySet().forEach(x -> {
if (x.equals(prefix) || (x.startsWith(prefix) && x.charAt(prefix.length()) == '.'))
set.add(x);
});
return set;
}
/**
* 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 null .
* "{!key}" if the key is missing.
*/
public String getString(String key, Object...args) {
String s = getString(key);
if (s.startsWith("{!"))
return s;
return format(s, 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 null if no value is found or the resource bundle is missing.
*/
public String findFirstString(String...keys) {
for (String k : keys) {
if (containsKey(k))
return getString(k);
}
return null;
}
@Override /* ResourceBundle */
protected Object handleGetObject(String key) {
String k = keyMap.get(key);
if (k == null)
return "{!" + key + "}";
try {
if (rbKeys.contains(k))
return rb.getObject(k);
} catch (MissingResourceException e) { /* Shouldn't happen */ }
return parent.handleGetObject(key);
}
@Override /* ResourceBundle */
public boolean containsKey(String key) {
return keyMap.containsKey(key);
}
@Override /* ResourceBundle */
public Set keySet() {
return keyMap.keySet();
}
@Override /* ResourceBundle */
public Enumeration getKeys() {
return Collections.enumeration(keySet());
}
@Override /* Object */
public String toString() {
JsonMap m = new JsonMap();
for (String k : new TreeSet<>(keySet()))
m.put(k, getString(k));
return Json5.of(m);
}
}