org.mypsycho.util.PropertiesLoader.java Source code

Java tutorial

Introduction

Here is the source code for org.mypsycho.util.PropertiesLoader.java

Source

/*
 * 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;
    }

}