/* * Copyright (C) 2011 Peransin Nicolas. * Use is subject to license terms. */ package org.mypsycho.text; import java.util.Arrays; import java.util.EnumMap; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.ResourceBundle.Control; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.beanutils.ContextClassLoaderLocal; /** * Formats a message using the resource bundle of associated enum. * <p> * A contract can be defined with named argument between code and constants if * the enum implements <code>EnumMessage.Message</code> interface.<br/> * If the enum does not provided named argument, the ordinal notation of * MessageFormat must be used (ie. {0}, {1}, ...). * </p> * Simple example : <br/> * Define the following properties file <code>/some/pack/PointMessage.properties</code> with * <pre> * location(name,point) = Point {name} is at ({point.x), {point.y)) * </pre> * And define the associated enum: * <pre> * package some.pack; * ... * public enum PointMessage implements BeanMessageFormat.Message { * location("name", "point"), ...; * * final String[] args; * PointMessage(String... pArgs) { args = pArgs; } * public String[] args() { args } * } * </pre> * Then you can call the message with some args: * <pre> * java.awt.Point p = new java.awt.Point(10, 20) * System.out.println(EnumMessage.format(PointMessage.location, "A", p); * </pre> * will display <code>Point A is at (10, 20)</code> * * @author Peransin Nicolas */ public class EnumMessage extends BeanMessageFormat { /** * */ private static final long serialVersionUID = -6577756882430832052L; public interface Message { String[] args(); } /** * Contains <code>BeanUtilsBean</code> instances indexed by context * classloader. */ private static final ContextClassLoaderLocal CACHE_BY_CLASSLOADER = new ContextClassLoaderLocal() { @Override protected Object initialValue() { return new HashMap<CacheKey, Cache<?>>(); } }; static private class Cache<K extends Enum<K>> extends EnumMap<K, String> { private static final long serialVersionUID = EnumMessage.serialVersionUID; public Cache(Class<K> clazz, Locale locale) { super(clazz); ResourceBundle bundle = null; try { bundle = ResourceBundle.getBundle(clazz.getName(), locale, clazz.getClassLoader(), Control.getControl(Control.FORMAT_PROPERTIES)); } catch (MissingResourceException e) { // use fall back value } for (K key : clazz.getEnumConstants()) { put(key, value(bundle, key)); } } String value(ResourceBundle bundle, K key) { // Build name String name = key.name(); String fallback = name; if (key instanceof Message) { String[] args = ((Message) key).args(); if ((args != null) && (args.length > 0)) { // anonym index boolean first = true; for (String arg : args) { fallback += (first ? "({" : "},{") + arg; name += (first ? '(' : ',') + arg; first = false; } name += ')'; fallback += "})"; } } if (bundle == null) { return fallback; } try { return bundle.getString(name); } catch (MissingResourceException noFullName) { try { return bundle.getString(key.name()); } catch (MissingResourceException noName) { return fallback; } } } } static private class CacheKey { Class<?> clazz; Locale locale; int hash; /** * */ public CacheKey(Class<?> c, Locale l) { clazz = c; locale = l; hash = c.hashCode() + l.hashCode(); } @Override public int hashCode() { return hash; } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (!(obj instanceof CacheKey)) { return false; } CacheKey other = (CacheKey) obj; return clazz.equals(other.clazz) && locale.equals(other.locale); } } static <K extends Enum<K>> Cache<?> getPatterns(Class<K> clazz, Locale locale) { @SuppressWarnings("unchecked") Map<CacheKey, Cache<?>> caches = (Map<CacheKey, Cache<?>>) CACHE_BY_CLASSLOADER.get(); CacheKey key = new CacheKey(clazz, locale); synchronized (caches) { Cache<?> cache = caches.get(key); if (cache == null) { cache = new Cache<K>(clazz, locale); caches.put(key, cache); } return cache; } } /** * Return the text associated to this enum * <p> * This method is public so user can avoid complex syntax if the message has no argument. * </p> * * @param messageId * @param locale * @return */ @SuppressWarnings("unchecked") public static <K extends Enum<?>> String getPattern(K messageId, Locale locale) { return (String) getPatterns(messageId.getClass(), locale).get(messageId); } static final Pattern indexPattern = Pattern.compile("(\\w+)(\\.(.*))?"); Enum<?> id; /** * Constructs a MessageFormat for the default locale and the * specified pattern. * The constructor first sets the locale, then parses the pattern and * creates a list of subformats for the format elements contained in it. * Patterns and their interpretation are specified in the * <a href="#patterns">class description</a>. * * @param messageId the pattern for this message format * @exception IllegalArgumentException if the pattern is invalid */ public EnumMessage(Enum<?> messageId) { this(messageId, Locale.getDefault()); } /** * Constructs a MessageFormat for the specified locale and * pattern. * The constructor first sets the locale, then parses the pattern and * creates a list of subformats for the format elements contained in it. * Patterns and their interpretation are specified in the * <a href="#patterns">class description</a>. * * @param messageId the pattern for this message format * @param locale the locale for this message format * @exception IllegalArgumentException if the pattern is invalid */ public EnumMessage(Enum<?> messageId, Locale locale) { super(getPattern(messageId, locale), locale); id = messageId; mapArgs(); } private void mapArgs() { for (ArgumentMap map : maps) { ((NamedMap) map).reindex(); } } @Override public void setLocale(Locale locale) { if (locale.equals(getLocale())) { return; // uselesss } applyPattern(getPattern(id, locale)); mapArgs(); super.setLocale(locale); } public static String format(Enum<? extends Message> messageId, Object... values) { return new EnumMessage(messageId).format(values); } @Override protected ArgumentMap createMap(String expr) { Matcher m = indexPattern.matcher(expr); if (!m.find()) { throw new IllegalArgumentException("can't parse argument number " + expr); } return new NamedMap(m.group(1), m.group(3)); } protected class NamedMap extends ArgumentMap { String name; protected NamedMap(String name, String expr) { super(-1, expr); this.name = name; } void reindex() throws IllegalArgumentException { String[] args = ((Message) id).args(); if ((args == null) || (args.length == 0)) { // anonym index reindexByNumber(); return; } index = Arrays.asList(args).indexOf(name); if (index < 0) { reindexByNumber(); } } private void reindexByNumber() throws IllegalArgumentException { try { index = Integer.parseInt(name); } catch (NumberFormatException e) { throw new IllegalArgumentException("can't parse argument number " + name); } if (index < 0) { throw new IllegalArgumentException("negative argument number " + name); } } } }