blob: a90d27e1bcd4eff33e89e031124f7925374d824d [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.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}.
*
* <p>
* Wraps a ResourceBundle to provide some useful additional functionality.
*
* <ul>
* <li>
* Instead of throwing {@link MissingResourceException}, the {@link ResourceBundle#getString(String)} method
* will return <js>"{!key}"</js> if the message could not be found.
* <li>
* Supported hierarchical lookup of resources from parent parent classes.
* <li>
* Support for easy retrieval of localized bundles.
* <li>
* Support for generalized resource bundles (e.g. properties files containing keys for several classes).
* </ul>
*
* <p>
* The following example shows the basic usage of this class for retrieving localized messages:
*
* <p class='bini'>
* <cc># Contents of MyClass.properties</cc>
* <ck>foo</ck> = <cv>foo {0}</cv>
* <ck>MyClass.bar</ck> = <cv>bar {0}</cv>
* </p>
* <p class='bjava'>
* <jk>public class</jk> MyClass {
* <jk>private static final</jk> Messages <jsf>MESSAGES</jsf> = Messages.<jsm>of</jsm>(MyClass.<jk>class</jk>);
*
* <jk>public void</jk> doFoo() {
*
* <jc>// A normal property.</jc>
* String <jv>foo</jv> = <jsf>MESSAGES</jsf>.getString(<js>"foo"</js>,<js>"x"</js>); <jc>// == "foo x"</jc>
*
* <jc>// A property prefixed by class name.</jc>
* String <jv>bar</jv> = <jsf>MESSAGES</jsf>.getString(<js>"bar"</js>,<js>"x"</js>); <jc>// == "bar x"</jc>
*
* <jc>// A non-existent property.</jc>
* String <jv>baz</jv> = <jsf>MESSAGES</jsf>.getString(<js>"baz"</js>,<js>"x"</js>); <jc>// == "{!baz}"</jc>
* }
* }
* </p>
*
* <p>
* The ability to resolve keys prefixed by class name allows you to place all your messages in a single file such
* as a common <js>"Messages.properties"</js> file along with those for other classes.
* <p>
* The following shows how to retrieve messages from a common bundle:
*
* <p class='bjava'>
* <jk>public class</jk> MyClass {
* <jk>private static final</jk> Messages <jsf>MESSAGES</jsf> = Messages.<jsm>of</jsm>(MyClass.<jk>class</jk>, <js>"Messages"</js>);
* }
* </p>
*
* <p>
* Resource bundles are searched using the following base name patterns:
* <ul>
* <li><js>"{package}.{name}"</js>
* <li><js>"{package}.i18n.{name}"</js>
* <li><js>"{package}.nls.{name}"</js>
* <li><js>"{package}.messages.{name}"</js>
* </ul>
*
* <p>
* These patterns can be customized using the {@link Builder#baseNames(String...)} method.
*
* <p>
* Localized messages can be retrieved in the following way:
*
* <p class='bjava'>
* <jc>// Return value from Japan locale bundle.</jc>
* String <jv>foo</jv> = <jsf>MESSAGES</jsf>.forLocale(Locale.<jsf>JAPAN</jsf>).getString(<js>"foo"</js>);
* </p>
*
* <h5 class='section'>See Also:</h5><ul>
* </ul>
*/
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. <js>"Messages"</js>).
* <br>If <jk>null</jk>, 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<Messages> {
Class<?> forClass;
Locale locale;
String name;
Messages parent;
List<Tuple2<Class<?>,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<Class<?>,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 <jk>null</jk>.
* @return This object.
*/
public Builder parent(Messages parent) {
this.parent = parent;
return this;
}
/**
* Specifies the bundle name (e.g. <js>"Messages"</js>).
*
* @param name
* The bundle name.
* <br>If <jk>null</jk>, 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.
* <br>The default is the following:
* <ul>
* <li><js>"{package}.{name}"</js>
* <li><js>"{package}.i18n.{name}"</js>
* <li><js>"{package}.nls.{name}"</js>
* <li><js>"{package}.messages.{name}"</js>
* </ul>
* @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 <jk>null</jk>, 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 <jk>null</jk>, 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;
}
// <FluentSetters>
@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;
}
// </FluentSetters>
//-------------------------------------------------------------------------------------------------------------
// 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<Locale,Messages> localizedMessages = new ConcurrentHashMap<>();
// Cache of virtual keys to actual keys.
private final Map<String,String> keyMap;
private final Set<String> 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<String,String> 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 <jk>null</jk>.
*/
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.
*
* <p>
* 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<String> keySet(String prefix) {
Set<String> 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 <jk>null</jk>.
* <js>"{!key}"</js> 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 <jk>null</jk> 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<String> keySet() {
return keyMap.keySet();
}
@Override /* ResourceBundle */
public Enumeration<String> 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);
}
}