
org.jdesktop.application.ResourceMap Maven / Gradle / Ivy
Show all versions of bsaf Show documentation
/*
* Copyright (C) 2006 Sun Microsystems, Inc. All rights reserved. Use is
* subject to license terms.
*/
package org.jdesktop.application;
import org.jdesktop.application.ResourceConverter.ResourceConverterException;
import org.jdesktop.application.utils.PlatformType;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Event;
import java.awt.Font;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.AbstractButton;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.KeyStroke;
import javax.swing.border.EmptyBorder;
/**
* A read-only encapsulation of one or more ResourceBundles that adds
* automatic string conversion, support for field and Swing component
* property injection, string resource variable substitution, and chaining.
*
* ResourceMaps are typically obtained with the {@code ApplicationContext}
* {@link ApplicationContext#getResourceMap getResourceMap} method
* which lazily creates per Application, package, and class ResourceMaps that
* are linked together with the ResourceMap parent property.
*
* An individual ResourceMap provides read-only access to all of the
* resources defined by the ResourceBundles named when the ResourceMap
* was created as well as all of its parent ResourceMaps. Resources
* are retrieved with the getObject method which requires both
* the name of the resource and its expected type. The latter is used
* to convert strings if neccessary.
* Converted values are cached. As a convenience, getObject
* wrapper methods for common GUI types, like getFont,
* and getColor, are provided.
*
* The getObject method scans raw string resource values
* for ${resourceName} variable substitutions before
* performing string conversion. Variables named this way can
* refer to String resources defined anywhere in their ResourceMap
* or any parent ResourceMap. The special variable ${null}
* means that the value of the resource will be null.
*
* ResourceMaps can be used to "inject" resource values into Swing
* component properties and into object fields. The
* injectComponents method uses Component names ({@link
* Component#setName}) to match resources names with properties. The
* injectFields method sets fields that have been tagged with
* the @Resource annotation to the value of resources
* with the same name.
*
* @author Hans Muller ([email protected])
* @see #injectComponents
* @see #injectFields
* @see ResourceConverter
* @see ResourceBundle
*/
public class ResourceMap {
private static Logger logger = Logger.getLogger(ResourceMap.class.getName());
public static final String KEY_PLATFORM = "platform";
private final static Object NULL_RESOURCE = "null resource";
private final ClassLoader classLoader;
private final ResourceMap parent;
private final List bundleNames;
private final String resourcesDir;
private Map bundlesMapP = null; // see getBundlesMap()
private Locale locale = Locale.getDefault(); // ...
private Set bundlesMapKeysP = null; // set getBundlesMapKeys()
private boolean bundlesLoaded = false; // ResourceBundles are loaded lazily
private PlatformType platform;
/**
* Creates a ResourceMap that contains all of the resources
* defined in the named {@link ResourceBundle}s as well as
* (recursively) the parent ResourceMap. The parent
* may be null. Typically just one ResourceBundle is specified
* however one might name additional ResourceBundles that contain
* platform or Swing look and feel specific resources. When multiple
* bundles are named, a resource defined in bundlen will
* overide the same resource defined in bundles0..n-1.
* In other words bundles named later in the argument list take
* precendence over the bundles named earlier.
*
* ResourceBundles are loaded with the specified ClassLoader. If
* classLoader is null, an IllegalArgumentException is
* thrown.
*
* At least one bundleName must be specified and all of the
* bundleNames must be non-empty strings, or an
* IllegalArgumentException is thrown. The bundles are
* listed in priority order, highest priority first. In other
* words, resources in the the first ResourceBundle named first,
* shadow resources with the same name later in the list.
*
* All of the bundleNames
* must share a common package prefix. The package prefix
* implicitly specifies the resources directory
* (see {@link #getResourcesDir}). For example, the resources
* directory for bundle names "myapp.resources.foo" and
* "myapp.resources.bar", would be "myapp/resources/". If
* bundle names don't share a common package prefix, then
* an IllegalArgumentException is thrown.
*
* @param parent parent ResourceMap or null
* @param classLoader the ClassLoader to be used to load the ResourceBundle
* @param bundleNames names of the ResourceBundle to be loaded
* @throws IllegalArgumentException if classLoader or any bundleName is
* null, if no bundleNames are specified, if any bundleName is an
* empty (zero length) String, or if all of the bundleNames don't
* have a common package prefix
* @see ResourceBundle
* @see #getParent
* @see #getClassLoader
* @see #getResourcesDir
* @see #getBundleNames
*/
public ResourceMap(ResourceMap parent, ClassLoader classLoader, List bundleNames) {
if (classLoader == null) {
throw new IllegalArgumentException("null ClassLoader");
}
if ((bundleNames == null) || (bundleNames.size() == 0)) {
throw new IllegalArgumentException("no bundle specified");
}
for (String bn : bundleNames) {
if ((bn == null) || (bn.length() == 0)) {
throw new IllegalArgumentException("invalid bundleName: \"" + bn + "\"");
}
}
String bpn = bundlePackageName(bundleNames.get(0));
for (String bn : bundleNames) {
if (!bpn.equals(bundlePackageName(bn))) {
throw new IllegalArgumentException("bundles not colocated: \"" + bn + "\" != \"" + bpn + "\"");
}
}
this.parent = parent;
this.classLoader = classLoader;
this.bundleNames = Collections.unmodifiableList(new ArrayList(bundleNames));
this.resourcesDir = bpn.replace(".", "/") + "/";
}
private String bundlePackageName(String bundleName) {
int i = bundleName.lastIndexOf(".");
return (i == -1) ? "" : bundleName.substring(0, i);
}
/**
* Just a convenience version of the constructor for the common case
* where there's only one bundle name. Defined as:
* this(parent, classLoader, Arrays.asList(bundleNames))
.
* @param parent
* @param classLoader
* @param bundleNames
*/
public ResourceMap(ResourceMap parent, ClassLoader classLoader, String... bundleNames) {
this(parent, classLoader, Arrays.asList(bundleNames));
}
/**
* Returns the parent ResourceMap, or null. Logically, this ResourceMap
* contains all of the resources defined here and (recursively) in the
* parent.
*
* @return the parent ResourceMap or null
*/
public ResourceMap getParent() {
return parent;
}
/**
* Returns the names of the ResourceBundles that define the
* resources contained by this ResourceMap.
*
* @return the names of the ResourceBundles in this ResourceMap
*/
public List getBundleNames() {
return bundleNames;
}
/**
* Returns the ClassLoader used to load the ResourceBundles for this
* ResourceMap.
*
* @return the classLoader constructor argument
*/
public ClassLoader getClassLoader() {
return classLoader;
}
/**
* Returns the resources directory that contains all of the ResourceBundles
* in this ResourceMap. It can be used with the the classLoader property
* to load files from the resources directory. For example:
*
* String filename = myResourceMap.getResourcesDir() + "myIcon.png";
* URL url = myResourceMap.getClassLoader().getResource(filename);
* new ImageIcon(iconURL);
*
*
* @return the the resources directory for this ResourceMap
*/
public String getResourcesDir() {
return resourcesDir;
}
/* Lazily flattens all of the ResourceBundles named in bundleNames
* into a single Map - bundlesMapP. The bundleNames list is in
* priority order, the first entry shadows later entries.
*/
private synchronized Map getBundlesMap() {
// If the default locale has changed, then reload
Locale defaultLocale = Locale.getDefault();
if (locale != defaultLocale) {
bundlesLoaded = false;
locale = defaultLocale;
}
if (!bundlesLoaded) {
String resourceSuffix = getPlatform().getResourceSuffix();
Map bundlesMap = new ConcurrentHashMap();
for (int i = bundleNames.size() - 1; i >= 0; i--) {
populateResourceMap(bundleNames.get(i), bundlesMap);
if (!resourceSuffix.isEmpty())
populateResourceMap(bundleNames.get(i)+"_"+resourceSuffix, bundlesMap);
}
bundlesMapP = bundlesMap;
bundlesLoaded = true;
}
return bundlesMapP;
}
private void populateResourceMap(String bundleName, Map bundlesMap) {
try {
ResourceBundle bundle = ResourceBundle.getBundle(bundleName, locale, classLoader);
Enumeration keys = bundle.getKeys();
while (keys.hasMoreElements()) {
String key = keys.nextElement();
bundlesMap.put(key, bundle.getObject(key));
}
} catch (MissingResourceException ignore) {
/* bundleName is just a location to check, it's not
* guaranteed to name a ResourceBundle
*/
}
}
private void checkNullKey(String key) {
if (key == null) {
throw new IllegalArgumentException("null key");
}
}
private synchronized Set getBundlesMapKeys() {
if (bundlesMapKeysP == null) {
Set allKeys = new HashSet(getResourceKeySet());
ResourceMap parent = getParent();
if (parent != null) {
allKeys.addAll(parent.keySet());
}
bundlesMapKeysP = Collections.unmodifiableSet(allKeys);
}
return bundlesMapKeysP;
}
/**
* Return a unmodifiable {@link Set} that contains all of the keys in
* this ResourceMap and (recursively) its parent ResourceMaps.
*
* @return all of the keys in this ResourceMap and its parent
* @see #getParent
*/
public Set keySet() {
return getBundlesMapKeys();
}
/**
* Returns true if this resourceMap or its parent (recursively) contains
* the specified key.
*
* @return true if this resourceMap or its parent contains the specified key.
* @see #getParent
* @see #keySet
*/
public boolean containsKey(String key) {
checkNullKey(key);
if (containsResourceKey(key)) {
return true;
} else {
ResourceMap parent = getParent();
return (parent != null) && parent.containsKey(key);
}
}
public PlatformType getPlatform() {
if (platform != null) return platform;
if (parent != null) return parent.getPlatform();
return PlatformType.DEFAULT;
}
public void setPlatform(PlatformType platform) {
if(platform == null) throw new IllegalArgumentException("Platform could not be null.");
if (this.platform != null) throw new IllegalStateException("The platform attribute is already set for this resource map.");
this.platform = platform;
}
/**
* Unchecked exception thrown by {@link #getObject} when resource lookup
* fails, for example because string conversion fails. This is
* not a missing resource exception. If a resource isn't defined
* for a particular key, getObject does not throw an exception.
*
* @see #getObject
*/
public static class LookupException extends RuntimeException {
private final Class type;
private final String key;
/**
* Constructs an instance of this class with some useful information
* about the failure.
*
* @param msg the detail message
* @param type the type of the resource
* @param key the name of the resource
*/
public LookupException(String msg, String key, Class type) {
super(String.format("%s: resource %s, type %s", msg, key, type));
this.key = key;
this.type = type;
}
/**
* Returns the type of the resource for which lookup failed.
* @return the resource type
*/
public Class getType() {
return type;
}
/**
* Returns the type of the name of resource for which lookup failed.
* @return the resource name
*/
public String getKey() {
return key;
}
}
/**
* By default this method is used by {@code keySet} to
* get the names of the resources defined in this ResourceMap.
* This method lazily loads the ResourceBundles named
* by the constructor.
*
* The protected {@code getResource}, {@code putResource}, and
* {@code containsResourceKey}, {@code getResourceKeySet} abstract
* the internal representation of this ResourceMap's list of
* {@code ResourceBundles}. Most applications can ignore them.
*
* @return the names of the resources defined in this ResourceMap
* @see #getResource
* @see #putResource
* @see #containsResourceKey
*/
protected Set getResourceKeySet() {
Map bundlesMap = getBundlesMap();
if (bundlesMap == null) {
return Collections.emptySet();
} else {
return bundlesMap.keySet();
}
}
/**
* By default this method is used by {@code getObject} to see
* if a resource is defined by this ResourceMap. This method lazily
* loads the ResourceBundles named by the constructor.
*
* The protected {@code getResource}, {@code putResource}, and
* {@code containsResourceKey}, {@code getResourceKeySet} abstract
* the internal representation of this ResourceMap's list of
* {@code ResourceBundles}. Most applications can ignore them.
*
* If {@code key} is null, an IllegalArgumentException is thrown.
*
* @param key the name of the resource
* @return true if a resource named {@code key} is defined in this ResourceMap
* @see #getResource
* @see #putResource
* @see #getResourceKeySet
*/
protected boolean containsResourceKey(String key) {
checkNullKey(key);
Map bundlesMap = getBundlesMap();
return (bundlesMap != null) && bundlesMap.containsKey(key);
}
/**
* By default this method is used by {@code getObject} to look up
* resource values in the internal representation of the {@code
* ResourceBundles} named when this ResourceMap was constructed.
* If a resource named {@code key} is {@link #containsResourceKey defined}
* then its value is returned, otherwise null.
* The {@code getResource} method lazily loads the
* ResourceBundles named by the constructor.
*
* The protected {@code getResource}, {@code putResource}, and
* {@code containsResourceKey}, {@code getResourceKeySet} abstract
* the internal representation of this ResourceMap's list of
* {@code ResourceBundles}. Most applications can ignore them.
*
* If {@code key} is null, an IllegalArgumentException is thrown.
*
* @param key the name of the resource
* @return the value of the resource named {@code key} (can be null)
* @see #putResource
* @see #containsResourceKey
* @see #getResourceKeySet
*/
protected Object getResource(String key) {
checkNullKey(key);
Map bundlesMap = getBundlesMap();
Object value = (bundlesMap != null) ? bundlesMap.get(key) : null;
return (value == NULL_RESOURCE) ? null : value;
}
/**
* By default this method is used by {@code getObject} to cache
* values that have been retrieved, evaluated (as in ${key}
* expressions), and string converted. A subclass could override
* this method to defeat caching or to refine the caching strategy.
* The {@code putResource} method lazily loads ResourceBundles.
*
* The protected {@code getResource}, {@code putResource}, and
* {@code containsResourceKey}, {@code getResourceKeySet} abstract
* the internal representation of this ResourceMap's list of
* {@code ResourceBundles}. Most applications can ignore them.
*
* If {@code key} is null, an IllegalArgumentException is thrown.
*
* @param key the name of the resource
* @param value the value of the resource (can be null)
* @see #getResource
* @see #containsResourceKey
* @see #getResourceKeySet
*/
protected void putResource(String key, Object value) {
checkNullKey(key);
if (KEY_PLATFORM.equals(key)) {
setPlatform((PlatformType) value);
} else {
Map bundlesMap = getBundlesMap();
if (bundlesMap != null) {
bundlesMap.put(key, (value == null) ? NULL_RESOURCE : value);
}
}
}
/**
* Returns the value of the resource named key, or null
* if no resource with that name exists. A resource exists if
* it's defined in this ResourceMap or (recursively) in the
* ResourceMap's parent.
*
* String resources may contain variables that name other
* resources. Each ${variable-key} variable is replaced
* with the value of a string resource named
* variable-key. For example, given the following
* resources:
*
* Application.title = My Application
* ErrorDialog.title = Error: ${application.title}
* WarningDialog.title = Warning: ${application.title}
*
* The value of "WarningDialog.title" would be
* "Warning: My Application". To include "${" in a
* resource, insert a backslash before the "$". For example, the
* value of escString in the example below, would
* be "${hello}":
*
* escString = \\${hello}
*
* Note that, in a properties file, the backslash character is
* used for line continuation, so we've had to escape that too.
* If the value of a resource is the special variable ${null},
* then the resource will be removed from this ResourceMap.
*
* The value returned by getObject will be of the specified type. If a
* string valued resource exists for key, and type is not
* String.class, the value will be converted using a
* ResourceConverter and the ResourceMap entry updated with the
* converted value.
*
* If the named resource exists and an error occurs during lookup,
* then a ResourceMap.LookupException is thrown. This can
* happen if string conversion fails, or if resource parameters
* can't be evaluated, or if the existing resource is of the wrong
* type.
*
* An IllegalArgumentException is thrown if key or type are null.
*
* @param key resource name
* @param type resource type
* @return the value of the resource
* @see #getParent
* @see ResourceConverter#forType
* @see ResourceMap.LookupException
* @throws LookupException if an error occurs during lookup or string conversion
* @throws IllegalArgumentException if key or type are null
*/
public Object getObject(String key, Class type) {
checkNullKey(key);
if (type == null) {
throw new IllegalArgumentException("null type");
}
if (type.isPrimitive()) {
if (type == Boolean.TYPE) {
type = Boolean.class;
} else if (type == Character.TYPE) {
type = Character.class;
} else if (type == Byte.TYPE) {
type = Byte.class;
} else if (type == Short.TYPE) {
type = Short.class;
} else if (type == Integer.TYPE) {
type = Integer.class;
} else if (type == Long.TYPE) {
type = Long.class;
} else if (type == Float.TYPE) {
type = Float.class;
} else if (type == Double.TYPE) {
type = Double.class;
}
}
Object value = null;
ResourceMap resourceMapNode = this;
/* Find the ResourceMap bundlesMap that contains a non-null
* value for the specified key, first check this ResourceMap,
* then its parents.
*/
while (resourceMapNode != null) {
if (resourceMapNode.containsResourceKey(key)) {
value = resourceMapNode.getResource(key);
break;
}
resourceMapNode = resourceMapNode.getParent();
}
/* If we've found a String expression then replace
* any ${key} variables, and then reset the
* the original resourceMapNode entry.
*/
if ((value instanceof String) && ((String) value).contains("${")) {
value = evaluateStringExpression((String) value);
resourceMapNode.putResource(key, value);
}
/* If the value we've found in resourceMapNode is
* the expected type, then we're done. If the expected
* type is primitive and the value is the corresponding
* object type then we're done too. Otherwise,
* if it's a String, then try and convert the String
* and replace the original resourceMapNode entry,
* otherwise return null.
*/
if (value != null) {
Class valueClass = value.getClass();
if (!type.isAssignableFrom(valueClass)) {
if (value instanceof String) {
ResourceConverter stringConverter = ResourceConverter.forType(type);
if (stringConverter != null) {
String sValue = (String) value;
try {
value = stringConverter.parseString(sValue, resourceMapNode);
resourceMapNode.putResource(key, value);
} catch (ResourceConverterException e) {
String msg = "string conversion failed";
LookupException lfe = new LookupException(msg, key, type);
lfe.initCause(e);
throw lfe;
}
} else {
String msg = "no StringConverter for required type";
throw new LookupException(msg, key, type);
}
} else {
String msg = "named resource has wrong type";
throw new LookupException(msg, key, type);
}
}
}
return value;
}
/* Given the following resources:
*
* hello = Hello
* world = World
* place = ${world}
*
* The value of evaluateStringExpression("${hello} ${place}")
* would be "Hello World". The value of ${null} is null.
*/
private String evaluateStringExpression(String expr) {
if (expr.trim().equals("${null}")) {
return null;
}
StringBuffer value = new StringBuffer();
int i0 = 0, i1;
while ((i1 = expr.indexOf("${", i0)) != -1) {
if ((i1 == 0) || ((i1 > 0) && (expr.charAt(i1 - 1) != '\\'))) {
int i2 = expr.indexOf("}", i1);
if ((i2 != -1) && (i2 > i1 + 2)) {
String k = expr.substring(i1 + 2, i2);
String v = getString(k);
value.append(expr.substring(i0, i1));
if (v != null) {
value.append(v);
} else {
String msg = String.format("no value for \"%s\" in \"%s\"", k, expr);
throw new LookupException(msg, k, String.class);
}
i0 = i2 + 1; // skip trailing "}"
} else {
String msg = String.format("no closing brace in \"%s\"", expr);
throw new LookupException(msg, "", String.class);
}
} else { // we've found an escaped variable - "\${"
value.append(expr.substring(i0, i1 - 1));
value.append("${");
i0 = i1 + 2; // skip past "${"
}
}
value.append(expr.substring(i0));
return value.toString();
}
/**
* If no arguments are specified, return the String value
* of the resource named key. This is
* equivalent to calling getObject(key, String.class)
* If arguments are provided, then the type of the resource
* named key is assumed to be
* {@link String#format(String, Object...) format} string,
* which is applied to the arguments if it's non null.
* For example, given the following resources
*
* hello = Hello %s
*
* then the value of getString("hello", "World") would
* be "Hello World".
*
* @param key
* @param args
* @return the String value of the resource named key
* @throws LookupException if an error occurs during lookup or string conversion
* @throws IllegalArgumentException if key is null
* @see #getObject
* @see String#format(String, Object...)
*/
public String getString(String key, Object... args) {
if (args.length == 0) {
return (String) getObject(key, String.class);
} else {
String format = (String) getObject(key, String.class);
return (format == null) ? null : String.format(format, args);
}
}
/**
* A convenience method that's shorthand for calling:
* getObject(key, Boolean.class).
*
* @param key the name of the resource
* @throws LookupException if an error occurs during lookup or string conversion
* @throws IllegalArgumentException if key is null
* @return the Boolean value of the resource named key
* @see #getObject
*/
public final Boolean getBoolean(String key) {
return (Boolean) getObject(key, Boolean.class);
}
/**
* A convenience method that's shorthand for calling:
* getObject(key, Integer.class).
*
* @param key the name of the resource
* @throws LookupException if an error occurs during lookup or string conversion
* @throws IllegalArgumentException if key is null
* @return the Integer value of the resource named key
* @see #getObject
*/
public final Integer getInteger(String key) {
return (Integer) getObject(key, Integer.class);
}
/**
* A convenience method that's shorthand for calling:
* getObject(key, Long.class).
*
* @param key the name of the resource
* @throws LookupException if an error occurs during lookup or string conversion
* @throws IllegalArgumentException if key is null
* @return the Long value of the resource named key
* @see #getObject
*/
public final Long getLong(String key) {
return (Long) getObject(key, Long.class);
}
/**
* A convenience method that's shorthand for calling:
* getObject(key, Short.class).
*
* @param key the name of the resource
* @throws LookupException if an error occurs during lookup or string conversion
* @throws IllegalArgumentException if key is null
* @return the Short value of the resource named key
* @see #getObject
*/
public final Short getShort(String key) {
return (Short) getObject(key, Short.class);
}
/**
* A convenience method that's shorthand for calling:
* getObject(key, Byte.class).
*
* @param key the name of the resource
* @throws LookupException if an error occurs during lookup or string conversion
* @throws IllegalArgumentException if key is null
* @return the Byte value of the resource named key
* @see #getObject
*/
public final Byte getByte(String key) {
return (Byte) getObject(key, Byte.class);
}
/**
* A convenience method that's shorthand for calling:
* getObject(key, Float.class).
*
* @param key the name of the resource
* @throws LookupException if an error occurs during lookup or string conversion
* @throws IllegalArgumentException if key is null
* @return the Float value of the resource named key
* @see #getObject
*/
public final Float getFloat(String key) {
return (Float) getObject(key, Float.class);
}
/**
* A convenience method that's shorthand for calling:
* getObject(key, Double.class).
*
* @param key the name of the resource
* @throws LookupException if an error occurs during lookup or string conversion
* @throws IllegalArgumentException if key is null
* @return the Double value of the resource named key
* @see #getObject
*/
public final Double getDouble(String key) {
return (Double) getObject(key, Double.class);
}
/**
*
* A convenience method that's shorthand for calling:
* getObject(key, Icon.class). This method
* relies on the ImageIcon ResourceConverter that's registered
* by this class. See {@link #getImageIcon} for more information.
*
*
* @param key the name of the resource
* @return the Icon value of the resource named key
* @see #getObject
* @throws LookupException if an error occurs during lookup or string conversion
* @throws IllegalArgumentException if key is null
*/
public final Icon getIcon(String key) {
return (Icon) getObject(key, Icon.class);
}
/**
*
* A convenience method that's shorthand for calling:
* getObject(key, ImageIcon.class). This method
* relies on the ImageIcon ResourceConverter that's registered
* by this class.
*
* If the resource named key is a String, it should name
* an image file to be found in the resources subdirectory that
* also contains the ResourceBundle (typically a ".properties"
* file) that was used to create the corresponding ResourceMap.
*
* For example, given the ResourceMap produced by
* Application.getClass(com.mypackage.MyClass.class),
* and a ResourceBundle called MyClass.properties
* in com.mypackage.resources:
*
* openIcon = myOpenIcon.png
*
* then resourceMap.getIcon("openIcon") would load
* the image file called "myOpenIcon.png" from the resources
* subdirectory, effectively like this:
*
* String filename = myResourceMap.getResourcesDir() + "myOpenIcon.png";
* URL url = myResourceMap.getClassLoader().getResource(filename);
* new ImageIcon(iconURL);
*
*
*
* @param key the name of the resource
* @return the ImageIcon value of the resource named key
* @see #getObject
* @throws LookupException if an error occurs during lookup or string conversion
* @throws IllegalArgumentException if key is null
*/
public final ImageIcon getImageIcon(String key) {
return (ImageIcon) getObject(key, ImageIcon.class);
}
/**
*
* A convenience method that's shorthand for calling:
* getObject(key, Font.class). This method relies
* on the Font ResourceConverter that's registered by this class.
* Font resources may be defined with strings that are
* recognized by {@link Font#decode},
* face-STYLE-size.
* For example:
*
* myFont = Arial-PLAIN-12
*
*
*
* @param key the name of the resource
* @return the Font value of the resource named key
* @see #getObject
* @see ResourceConverter#forType
* @see Font#decode
* @throws LookupException if an error occurs during lookup or string conversion
* @throws IllegalResourceConverteron if key is null
*/
public final Font getFont(String key) {
return (Font) getObject(key, Font.class);
}
/**
*
* A convenience method that's shorthand for calling:
* getObject(key, Color.class). This method relies on the
* Color ResourceConverter that's registered by this class. It defines
* an improved version of Color.decode()
* that supports colors with an alpha channel and comma
* separated RGB[A] values. Legal format for color resources are:
*
* myHexRGBColor = #RRGGBB
* myHexAlphaRGBColor = #AARRGGBB
* myRGBColor = R, G, B
* myAlphaRGBColor = R, G, B, A
*
* The first two examples, with the leading "#" encode the color
* with 3 or 4 hex values and the latter with integer values between
* 0 and 255. In both cases the value represented by "A" is the
* color's (optional) alpha channel.
*
*
* @param key the name of the resource
* @return the Color value of the resource named key
* @see #getObject
* @see ResourceConverter#forType
* @throws LookupException if an error occurs during lookup or string conversion
* @throws IllegalArgumentException ResourceConverter is null
*/
public final Color getColor(String key) {
return (Color) getObject(key, Color.class);
}
/**
*
* A convenience method that's shorthand for calling:
* getObject(key, KeyStroke.class). This method relies on the
* KeyStroke ResourceConverter that's registered by this class and
* uses {@link KeyStroke#getKeyStroke(String s)} to convert strings.
*
* For example, pressed F reports the "F" key, and control
* pressed F reports Control-F. See the KeyStroke JavaDoc for
* more information.
*
* @param key the name of the resource
* @return the KeyStroke value of the resource named key
* @see #getObject
* @see KeyStroke#getKeyStroke
* @throws LookupException if an error occurs during lookup or string conversion
* @throws IllegalArgumentException if key is null
*/
public final KeyStroke getKeyStroke(String key) {
return (KeyStroke) getObject(key, KeyStroke.class);
}
/**
* A convenience method that's shorthand for calling:
* getKeyStroke(key).getKeyCode(). If there's
* no resource named key then null is returned.
*
* @param key the name of the resource
* @throws LookupException if an error occurs during lookup or string conversion
* @throws IllegalArgumentException if key is null
* @return the KeyCode value of the resource named key
* @see #getObject
*/
public Integer getKeyCode(String key) {
KeyStroke ks = getKeyStroke(key);
return (ks != null) ? ks.getKeyCode() : null;
}
/**
* Unchecked exception thrown by {@link #injectComponent} and
* {@link #injectComponents} when a property value specified by
* a resource can not be set.
*
* @see #injectComponent
* @see #injectComponents
*/
public static class PropertyInjectionException extends RuntimeException {
private final String key;
private final Component component;
private final String propertyName;
/**
* Constructs an instance of this class with some useful information
* about the failure.
*
* @param msg the detail message
* @param key the name of the resource
* @param component the component whose property couldn't be set
* @param propertyName the name of the component property
*/
public PropertyInjectionException(String msg, String key, Component component, String propertyName) {
super(String.format("%s: resource %s, property %s, component %s", msg, key, propertyName, component));
this.key = key;
this.component = component;
this.propertyName = propertyName;
}
/**
* Returns the the name of resource whose value was to be used to set the property
* @return the resource name
*/
public String getKey() {
return key;
}
/**
* Returns the component whose property could not be set
* @return the component
*/
public Component getComponent() {
return component;
}
/**
* Returns the the name of property that could not be set
* @return the property name
*/
public String getPropertyName() {
return propertyName;
}
}
private void injectComponentProperty(Component component, PropertyDescriptor pd, String key) {
Method setter = pd.getWriteMethod();
Class type = pd.getPropertyType();
if ((setter != null) && (type != null) && containsKey(key)) {
Object value = getObject(key, type);
String propertyName = pd.getName();
try {
// Note: this could be generalized, we could delegate
// to a component property injector.
if ("text".equals(propertyName) && (component instanceof AbstractButton)) {
MnemonicText.configure(component, (String) value);
} else if ("text".equals(propertyName) && (component instanceof JLabel)) {
MnemonicText.configure(component, (String) value);
} else {
setter.invoke(component, value);
}
} catch (Exception e) {
String pdn = pd.getName();
String msg = "property setter failed";
RuntimeException re = new PropertyInjectionException(msg, key, component, pdn);
re.initCause(e);
throw re;
}
} else if (type != null) {
String pdn = pd.getName();
String msg = "no value specified for resource";
throw new PropertyInjectionException(msg, key, component, pdn);
} else if (setter == null) {
String pdn = pd.getName();
String msg = "can't set read-only property";
throw new PropertyInjectionException(msg, key, component, pdn);
}
}
private void injectComponentProperties(Component component) {
String componentName = component.getName();
if (componentName != null) {
/* Optimization: punt early if componentName doesn't
* appear in any componentName.propertyName resource keys
*/
boolean matchingResourceFound = false;
for (String key : keySet()) {
int i = key.lastIndexOf(".");
if ((i != -1) && componentName.equals(key.substring(0, i))) {
matchingResourceFound = true;
break;
}
}
if (!matchingResourceFound) {
return;
}
BeanInfo beanInfo;
try {
beanInfo = Introspector.getBeanInfo(component.getClass());
} catch (IntrospectionException e) {
String msg = "introspection failed";
RuntimeException re = new PropertyInjectionException(msg, null, component, null);
re.initCause(e);
throw re;
}
PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
if ((pds != null) && (pds.length > 0)) {
for (String key : keySet()) {
int i = key.lastIndexOf(".");
String keyComponentName = (i == -1) ? null : key.substring(0, i);
if (componentName.equals(keyComponentName)) {
if ((i + 1) == key.length()) {
/* key has no property name suffix, e.g. "myComponentName."
* This is probably a mistake.
*/
String msg = "component resource lacks property name suffix";
logger.warning(msg);
break;
}
String propertyName = key.substring(i + 1);
boolean matchingPropertyFound = false;
for (PropertyDescriptor pd : pds) {
if (pd.getName().equals(propertyName)) {
injectComponentProperty(component, pd, key);
matchingPropertyFound = true;
break;
}
}
if (!matchingPropertyFound) {
String msg = String.format(
"[resource %s] component named %s doesn't have a property named %s",
key, componentName, propertyName);
logger.warning(msg);
}
}
}
}
}
}
/**
* Set each property in target to the value of
* the resource named componentName.propertyName,
* where componentName is the value of the
* target component's name property, i.e. the value of
* target.getName(). The type of the resource must
* match the type of the corresponding property. Properties
* that aren't defined by a resource aren't set.
*
* For example, given a button configured like this:
*
* myButton = new JButton();
* myButton.setName("myButton");
*
* And a ResourceBundle properties file with the following
* resources:
*
* myButton.text = Hello World
* myButton.foreground = 0, 0, 0
* myButton.preferredSize = 256, 256
*
* Then injectComponent(myButton) would initialize
* myButton's text, foreground, and preferredSize properties
* to Hello World, new Color(0,0,0), and
* new Dimension(256,256) respectively.
*
* This method calls {@link #getObject} to look up resources
* and it uses {@link Introspector#getBeanInfo} to find
* the target component's properties.
*
* If target is null an IllegalArgumentException is thrown. If a
* resource is found that matches the target component's name but
* the corresponding property can't be set, an (unchecked) {@link
* PropertyInjectionException} is thrown.
*
*
*
* @param target the Component to inject
* @see #injectComponents
* @see #getObject
* @see ResourceConverter#forType
* @throws LookupException if an error occurs during lookup or string conversion
* @throws PropertyInjectionException if a property specified by a resource can't be set
* @throws IllegalArgumentException if target is null
*/
public void injectComponent(Component target) {
if (target == null) {
throw new IllegalArgumentException("null target");
}
injectComponentProperties(target);
}
/**
* Applies {@link #injectComponent} to each Component in the
* hierarchy with root root.
*
* @param root the root of the component hierarchy
* @throws PropertyInjectionException if a property specified by a resource can't be set
* @throws IllegalArgumentException if target is null
* @see #injectComponent
*/
public void injectComponents(Component root) {
injectComponent(root);
if (root instanceof JMenu) {
/* Warning: we're bypassing the popupMenu here because
* JMenu#getPopupMenu creates it; doesn't seem right
* to do so at injection time. Unfortunately, this
* means that attempts to inject the popup menu's
* "label" property will fail.
*/
JMenu menu = (JMenu) root;
for (Component child : menu.getMenuComponents()) {
injectComponents(child);
}
} else if (root instanceof Container) {
Container container = (Container) root;
for (Component child : container.getComponents()) {
injectComponents(child);
}
}
}
/**
* Unchecked exception thrown by {@link #injectFields} when
* an error occurs while attempting to set a field (a field that
* had been marked with @Resource).
*
* @see #injectFields
*/
public static class InjectFieldException extends RuntimeException {
private final Field field;
private final Object target;
private final String key;
/**
* Constructs an instance of this class with some useful information
* about the failure.
*
* @param msg the detail message
* @param field the Field we were attempting to set
* @param target the object whose field we were attempting to set
* @param key the name of the resource
*/
public InjectFieldException(String msg, Field field, Object target, String key) {
super(String.format("%s: resource %s, field %s, target %s", msg, key, field, target));
this.field = field;
this.target = target;
this.key = key;
}
/**
* Return the Field whose value couldn't be set.
* @return the field whose value couldn't be set
*/
public Field getField() {
return field;
}
/**
* Return the Object whose Field we were attempting to set
* @return the Object whose Field we were attempting to set
*/
public Object getTarget() {
return target;
}
/**
* Returns the type of the name of resource for which lookup failed.
* @return the resource name
*/
public String getKey() {
return key;
}
}
private void injectField(Field field, Object target, String key) {
Class type = field.getType();
if (type.isArray()) {
type = type.getComponentType();
Pattern p = Pattern.compile(key + "\\[([\\d]+)\\]"); // matches key[12]
for (String arrayElementKey : keySet()) {
Matcher m = p.matcher(arrayElementKey);
if (m.matches()) {
/* field's value is an array, arrayElementKey is a resource
* name of the form "MyClass.myArray[12]" and m.group(1)
* matches the array index. Set the index element
* of the field's array to the value of the resource.
*/
Object value = getObject(arrayElementKey, type);
if (!field.isAccessible()) {
field.setAccessible(true);
}
try {
int index = Integer.parseInt(m.group(1));
Array.set(field.get(target), index, value);
} /* Array.set throws IllegalArgumentException, ArrayIndexOutOfBoundsException
* field.get throws IllegalAccessException(Checked), IllegalArgumentException
* Integer.parseInt throws NumberFormatException (Checked)
*/ catch (Exception e) {
String msg = "unable to set array element";
InjectFieldException ife = new InjectFieldException(msg, field, target, key);
ife.initCause(e);
throw ife;
}
}
}
} else { // field is not an array
Object value = getObject(key, type);
if (value != null) {
if (!field.isAccessible()) {
field.setAccessible(true);
}
try {
field.set(target, value);
} /* Field.set throws IllegalAccessException, IllegalArgumentException,
* ExceptionInInitializerError
*/ catch (Exception e) {
String msg = "unable to set field's value";
InjectFieldException ife = new InjectFieldException(msg, field, target, key);
ife.initCause(e);
throw ife;
}
}
}
}
/**
* Set each field with a @Resource annotation in the target object,
* to the value of a resource whose name is the simple name of the target
* class followed by "." followed by the name of the field. If the
* key @Resource parameter is specified, then a resource with that name
* is used instead. Array valued fields can also be initialized
* with resources whose names end with "[index]". For example:
*
* class MyClass {
* @Resource String sOne;
* @Resource(key="sTwo") String s2;
* @Resource int[] numbers = new int[2];
* }
*
* Given the previous class and the following resource file:
*
* MyClass.sOne = One
* sTwo = Two
* MyClass.numbers[0] = 10
* MyClass.numbers[1] = 11
*
* Then injectFields(new MyClass()) would initialize the MyClass
* sOne field to "One", the s2 field to "Two", and the
* two elements of the numbers array to 10 and 11.
*
* If target is null an IllegalArgumentException is
* thrown. If an error occurs during resource lookup, then an
* unchecked LookupException is thrown. If a target field marked
* with @Resource can't be set, then an unchecked
* InjectFieldException is thrown.
*
* @param target the object whose fields will be initialized
* @throws LookupException if an error occurs during lookup or string conversion
* @throws InjectFieldException if a field can't be set
* @throws IllegalArgumentException if target is null
* @see #getObject
*/
public void injectFields(Object target) {
if (target == null) {
throw new IllegalArgumentException("null target");
}
Class targetType = target.getClass();
if (targetType.isArray()) {
throw new IllegalArgumentException("array target");
}
String keyPrefix = targetType.getSimpleName() + ".";
for (Field field : targetType.getDeclaredFields()) {
Resource resource = field.getAnnotation(Resource.class);
if (resource != null) {
String rKey = resource.key();
String key = (rKey.length() > 0) ? rKey : keyPrefix + field.getName();
injectField(field, target, key);
}
}
}
/* Register ResourceConverters that are defined in this class
* and documented here.
*/
static {
ResourceConverter[] stringConverters = {
new ColorStringConverter(),
new IconStringConverter(),
new ImageStringConverter(),
new FontStringConverter(),
new KeyStrokeStringConverter(),
new DimensionStringConverter(),
new PointStringConverter(),
new RectangleStringConverter(),
new InsetsStringConverter(),
new EmptyBorderStringConverter()
};
for (ResourceConverter sc : stringConverters) {
ResourceConverter.register(sc);
}
}
/* If path doesn't have a leading "/" then the resourcesDir
* is prepended, otherwise the leading "/" is removed.
*/
private static String resourcePath(final String path, ResourceMap resourceMap) {
if (path == null) {
return null;
} else if (path.startsWith("/")) {
return (path.length() > 1) ? path.substring(1) : null;
} else {
return resourceMap.getResourcesDir() + path;
}
}
private static ImageIcon loadImageIcon(String s, ResourceMap resourceMap)
throws ResourceConverterException {
String rPath = resourcePath(s, resourceMap);
if (rPath == null) {
String msg = String.format("invalid image/icon path \"%s\"", s);
throw new ResourceConverterException(msg, s);
}
URL url = resourceMap.getClassLoader().getResource(rPath);
if (url != null) {
return new ImageIcon(url);
} else {
String msg = String.format("couldn't find Icon resource \"%s\"", s);
throw new ResourceConverterException(msg, s);
}
}
private static class FontStringConverter extends ResourceConverter {
FontStringConverter() {
super(Font.class);
}
/* Just delegates to Font.decode.
* Typical string is: face-STYLE-size, for example "Arial-PLAIN-12"
*/
@Override
public Object parseString(String s, ResourceMap ignore) throws ResourceConverterException {
return Font.decode(s);
}
}
private static class ColorStringConverter extends ResourceConverter {
ColorStringConverter() {
super(Color.class);
}
// private void error(String msg, String s, Exception e) throws ResourceConverterException {
// throw new ResourceConverterException(msg, s, e);
// }
/* An improved version of Color.decode() that supports colors
* with an alpha channel and comma separated RGB[A] values.
* Legal format for color resources are:
* "#RRGGBB", "#AARRGGBB", "R, G, B", "R, G, B, A"
* Thanks to Romain Guy for the code.
*/
@Override
public Object parseString(String s, ResourceMap ignore) throws ResourceConverterException {
final Color color;
if (s.startsWith("#")) {
switch (s.length()) {
// RGB/hex color
case 7:
color = Color.decode(s);
break;
// ARGB/hex color
case 9:
int alpha = Integer.decode(s.substring(0, 3));
int rgb = Integer.decode("#" + s.substring(3));
color = new Color(alpha << 24 | rgb, true);
break;
default:
throw new ResourceConverterException("invalid #RRGGBB or #AARRGGBB color string", s);
}
} else {
String[] parts = s.split(",");
if (parts.length < 3 || parts.length > 4) {
throw new ResourceConverterException("invalid R, G, B[, A] color string", s);
}
try {
// with alpha component
if (parts.length == 4) {
int r = Integer.parseInt(parts[0].trim());
int g = Integer.parseInt(parts[1].trim());
int b = Integer.parseInt(parts[2].trim());
int a = Integer.parseInt(parts[3].trim());
color = new Color(r, g, b, a);
} else {
int r = Integer.parseInt(parts[0].trim());
int g = Integer.parseInt(parts[1].trim());
int b = Integer.parseInt(parts[2].trim());
color = new Color(r, g, b);
}
} catch (NumberFormatException e) {
throw new ResourceConverterException("invalid R, G, B[, A] color string", s, e);
}
}
return color;
}
}
private static class IconStringConverter extends ResourceConverter {
IconStringConverter() {
super(Icon.class);
}
@Override
public Object parseString(String s, ResourceMap resourceMap) throws ResourceConverterException {
return loadImageIcon(s, resourceMap);
}
@Override
public boolean supportsType(Class testType) {
return testType.equals(Icon.class) || testType.equals(ImageIcon.class);
}
}
private static class ImageStringConverter extends ResourceConverter {
ImageStringConverter() {
super(Image.class);
}
@Override
public Object parseString(String s, ResourceMap resourceMap) throws ResourceConverterException {
return loadImageIcon(s, resourceMap).getImage();
}
}
private static class KeyStrokeStringConverter extends ResourceConverter {
KeyStrokeStringConverter() {
super(KeyStroke.class);
}
@Override
public Object parseString(String s, ResourceMap ignore) {
if (s.contains("shortcut")) {
int k = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
s = s.replaceAll("shortcut", (k == Event.META_MASK) ? "meta" : "control");
}
return KeyStroke.getKeyStroke(s);
}
}
/* String s is assumed to contain n number substrings separated by
* commas. Return a list of those integers or null if there are too
* many, too few, or if a substring can't be parsed. The format
* of the numbers is specified by Double.valueOf().
*/
private static List parseDoubles(String s, int n, String errorMsg) throws ResourceConverterException {
String[] doubleStrings = s.split(",", n + 1);
if (doubleStrings.length != n) {
throw new ResourceConverterException(errorMsg, s);
} else {
List doubles = new ArrayList(n);
for (String doubleString : doubleStrings) {
try {
doubles.add(Double.valueOf(doubleString));
} catch (NumberFormatException e) {
throw new ResourceConverterException(errorMsg, s, e);
}
}
return doubles;
}
}
private static class DimensionStringConverter extends ResourceConverter {
DimensionStringConverter() {
super(Dimension.class);
}
@Override
public Object parseString(String s, ResourceMap ignore) throws ResourceConverterException {
List xy = parseDoubles(s, 2, "invalid x,y Dimension string");
Dimension d = new Dimension();
d.setSize(xy.get(0), xy.get(1));
return d;
}
}
private static class PointStringConverter extends ResourceConverter {
PointStringConverter() {
super(Point.class);
}
@Override
public Object parseString(String s, ResourceMap ignore) throws ResourceConverterException {
List xy = parseDoubles(s, 2, "invalid x,y Point string");
Point p = new Point();
p.setLocation(xy.get(0), xy.get(1));
return p;
}
}
private static class RectangleStringConverter extends ResourceConverter {
RectangleStringConverter() {
super(Rectangle.class);
}
@Override
public Object parseString(String s, ResourceMap ignore) throws ResourceConverterException {
List xywh = parseDoubles(s, 4, "invalid x,y,width,height Rectangle string");
Rectangle r = new Rectangle();
r.setFrame(xywh.get(0), xywh.get(1), xywh.get(2), xywh.get(3));
return r;
}
}
private static class InsetsStringConverter extends ResourceConverter {
InsetsStringConverter() {
super(Insets.class);
}
@Override
public Object parseString(String s, ResourceMap ignore) throws ResourceConverterException {
List tlbr = parseDoubles(s, 4, "invalid top,left,bottom,right Insets string");
return new Insets(tlbr.get(0).intValue(), tlbr.get(1).intValue(), tlbr.get(2).intValue(), tlbr.get(3).intValue());
}
}
private static class EmptyBorderStringConverter extends ResourceConverter {
EmptyBorderStringConverter() {
super(EmptyBorder.class);
}
@Override
public Object parseString(String s, ResourceMap ignore) throws ResourceConverterException {
List tlbr = parseDoubles(s, 4, "invalid top,left,bottom,right EmptyBorder string");
return new EmptyBorder(tlbr.get(0).intValue(), tlbr.get(1).intValue(), tlbr.get(2).intValue(), tlbr.get(3).intValue());
}
}
}