/* * Copyright (C) 2011 Peransin Nicolas. * Use is subject to license terms. */ package org.mypsycho.util; import java.util.Collection; import java.util.Deque; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.Properties; import java.util.ResourceBundle; import java.util.Set; import org.apache.commons.collections.FastHashMap; import org.mypsycho.beans.WeakFastHashMap; /** * XXX Doc * <p>Detail ... </p> * @author Peransin Nicolas */ public class PropertiesLoader { public static final String SUBST_TOKEN = "${"; public static final char ESCAPE = SUBST_TOKEN.charAt(0); public static final String END_TOKEN = "}"; public static final char MEMBER_TOKEN = '#'; public static final char FALLBACK_TOKEN = '?'; public interface LoadingListener { void handle(Object event, String detail, Throwable t); } protected String createKey(String basename, Locale locale) { return "file:/" + basename + "?" + locale; } protected String createKey(Class<?> type, Locale locale) { return "class:/" + type.getName() + "?" + locale; } protected class Bundle implements Map<String, String> { private FastHashMap map = new FastHashMap(); private Locale locale; private String basename; private ClassLoader context; Bundle(String name, Locale l, ClassLoader loader) { locale = l; basename = name; context = loader; } /** * Returns the locale. * * @return the locale */ public Locale getLocale() { return locale; } /** * Returns the name. * * @return the name */ public String getBasename() { return basename; } /** * Returns the context. * * @return the context */ public ClassLoader getContext() { return context; } public int size() { return map.size(); } public boolean isEmpty() { return map.isEmpty(); } public boolean containsKey(Object key) { return map.containsKey(key); } public boolean containsValue(Object value) { throw new UnsupportedOperationException(); } public String get(Object key) { Object o = map.get(key); if (o == null) { return null; } if (o instanceof String) { return (String) o; } if (!(key instanceof String)) { return null; } return resolveProperty(this, (String) key, new LinkedList<String>()); } Object getDefinition(Object key) { return map.get(key); } String putValue(String key, String value) { map.put(key, value); return null; } String putDefinition(String key, String value) { map.put(key, new String[] { value }); return null; } public String put(String key, String value) { throw new UnsupportedOperationException(); } public String remove(Object key) { throw new UnsupportedOperationException(); } public void putAll(Map<? extends String, ? extends String> m) { throw new UnsupportedOperationException(); } public void clear() { throw new UnsupportedOperationException(); } @SuppressWarnings("unchecked") public Set<String> keySet() { return map.keySet(); } @SuppressWarnings("unchecked") public Collection<String> values() { resolve(); return map.values(); } @SuppressWarnings("unchecked") public Set<java.util.Map.Entry<String, String>> entrySet() { resolve(); return map.entrySet(); } @SuppressWarnings("unchecked") void resolve() { if (map.getFast()) { // already resolved return; } // Copy is required as the map is updated for (Object key : new HashSet<Object>(map.keySet())) { get(key); // resolve } map.setFast(true); } @Override public String toString() { return map.toString(); } } protected Class<?> until(Class<?> type) { return null; } Map<String, Bundle> cache; // Weak references private static final ResourceBundle.Control RES_CONTROL = ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_PROPERTIES); LoadingListener listener = null; Properties env = null; public PropertiesLoader() { cache = new WeakFastHashMap<String, Bundle>(); ((WeakFastHashMap<?, ?>) cache).setFast(true); } public PropertiesLoader(String prefix, Properties globals) { this(); addGlobals(prefix, globals); } public PropertiesLoader(Properties globals) { this("", globals); } public void addGlobals(Map<?, ?> globals) { addGlobals("", globals); } public void addGlobals(String prefix, Map<?, ?> globals) { for (Map.Entry<?, ?> entry : globals.entrySet()) { addGlobal(prefix + entry.getKey(), String.valueOf(entry.getValue())); } } public synchronized void addGlobal(String key, String value) { if (env == null) { env = new Properties(); } env.setProperty(key, value); cache.clear(); // Previous resolution are deprecated // In fact, we should marke all bundles as invalid } protected void handle(Object event, String detail) { LoadingListener l = listener; if (l != null) { l.handle(event, detail, null); } } public Map<String, String> getProperties(Class<?> type, Locale locale) { Bundle props = getBundleImpl(type, locale); props.resolve(); // optimisation for lock return props; } protected Bundle getBundleImpl(Class<?> type, Locale locale) { String cacheKey = createKey(type, locale); Bundle props = cache.get(cacheKey); if (props != null) { return props; } props = createBundle(type, locale); cache.put(cacheKey, props); return props; } protected Bundle createBundle(Class<?> type, Locale locale) { Bundle props = new Bundle(type.getName(), locale, type.getClassLoader()); Class<?> until = until(type); if (until == null) { until = Object.class; } List<ResourceBundle> bundles = new LinkedList<ResourceBundle>(); for (Class<?> c = type; (c != null) && !c.equals(until); c = c.getSuperclass()) { String basename = c.getName(); try { bundles.add(0, ResourceBundle.getBundle(basename, locale, getClassLoader(c), RES_CONTROL)); } catch (MissingResourceException e) { handle("noBundle", basename); } } for (ResourceBundle bundle : bundles) { for (String key : bundle.keySet()) { props.putDefinition(key, bundle.getString(key)); } } return props; } private ClassLoader getClassLoader(Class<?> type) { ClassLoader loader = type.getClassLoader(); return (loader != null) ? loader : ClassLoader.getSystemClassLoader(); } protected Bundle getBundle(String basename, Locale locale, ClassLoader loader) { try { Class<?> container = Class.forName(basename, true, loader); return getBundleImpl(container, locale); } catch (ClassNotFoundException e) { // ignore, we use file without class } String cacheKey = createKey(basename, locale); Bundle props = cache.get(cacheKey); if (props != null) { return props; } // try to load the class ?? props = createBundle(basename, locale, loader); cache.put(cacheKey, props); return props; } protected Bundle createBundle(String basename, Locale locale, ClassLoader loader) { Bundle props = new Bundle(basename, locale, loader); try { ResourceBundle bundle = ResourceBundle.getBundle(basename, locale, loader, RES_CONTROL); for (String key : bundle.keySet()) { props.putDefinition(key, bundle.getString(key)); } } catch (MissingResourceException e) { handle("noBundle", basename); } return props; } protected String resolveProperty(Bundle bundle, String key, Deque<String> refStack) { String fullKey = key; String localKey = key; int indexBundle = key.indexOf(MEMBER_TOKEN); Bundle definingBundle = bundle; if (indexBundle < 0) { // local name fullKey = bundle.getBasename() + MEMBER_TOKEN + key; } else if ((indexBundle > 0) && (key.indexOf(MEMBER_TOKEN, indexBundle + 1) < 0)) { String basename = key.substring(0, indexBundle); localKey = key.substring(indexBundle + 1); definingBundle = getBundle(basename, bundle.getLocale(), bundle.getContext()); } else { handle("malformedKey", key + " in " + bundle.getBasename()); } // else not a cross reference fullKey == localKey == key if (refStack.contains(fullKey)) { handle("recursivity", fullKey); return SUBST_TOKEN + key + END_TOKEN; } Object value = null; for (int fb = localKey.length(); (value == null) && (fb != -1); fb = localKey.lastIndexOf(FALLBACK_TOKEN, fb - 1)) { value = definingBundle.getDefinition(localKey.substring(0, fb)); } // value = definingBundle.getDefinition(localKey); if (value == null) { // not defined if (env != null) { // Extension Point value = env.getProperty(key); if (value != null) { return (String) value; } } handle("undefined", fullKey); return SUBST_TOKEN + key + END_TOKEN; } if (value instanceof String) { // already substituted return (String) value; } // else unresolved value refStack.addLast(fullKey); String newValue = resolveExpression(definingBundle, ((String[]) value)[0], refStack); refStack.removeLast(); definingBundle.putValue(localKey, newValue); return newValue; } protected String resolveExpression(Bundle bundle, String expr, Deque<String> refStack) { int vegas = 0; // Vegas keeps a trace of what has been substitued // Note: What Happens in Vegas, Stays in Vegas. for (int i = expr.indexOf(SUBST_TOKEN); i != -1; i = expr.indexOf(SUBST_TOKEN, i)) { int escaping = 0; while (((i - escaping) > vegas) && (expr.charAt(i - escaping - 1) == ESCAPE)) { escaping++; } if (escaping > 0) { String head = expr.substring(0, i - escaping); String tail = expr.substring(i); StringBuffer escaped = new StringBuffer(); while (escaping > 1) { escaped.append(ESCAPE); escaping = escaping - 2; } expr = head + escaped + tail; vegas = head.length() + escaped.length(); // +1 : escape car i = vegas; if (escaping == 1) { vegas++; i++; continue; } // else 0: no escape } int end = -1; // Seek end of token int position = i + SUBST_TOKEN.length(); int depth = 0; // Inclusion flag while (end == -1) { int nextRef = expr.indexOf(SUBST_TOKEN, position); int nextEnd = expr.indexOf(END_TOKEN, position); if (nextEnd == -1) { handle("IllegalExpression", expr); return expr; } else if ((nextRef != -1) && (nextRef < nextEnd)) { depth++; position = nextRef + SUBST_TOKEN.length(); } else if (depth > 0) { depth--; position = nextEnd + END_TOKEN.length(); } else { // depth == 0 end = nextEnd; } } // Cutting the string String head = expr.substring(0, i); String tail = expr.substring(end + END_TOKEN.length(), expr.length()); String var = expr.substring(i + SUBST_TOKEN.length(), end); var = resolveExpression(bundle, var, refStack); String subst = resolveProperty(bundle, var, refStack); expr = head + subst + tail; i = head.length() + subst.length(); vegas = i; } return expr; } }