package jtrade.util; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.net.InetAddress; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Configurable<T> { private static final Logger logger = LoggerFactory.getLogger(Configurable.class); private static ConcurrentHashMap<String, Configurable<?>> configurables = new ConcurrentHashMap<String, Configurable<?>>(); private static Properties configuration; /** * Declared name of the configurable in container class */ private String name; /** * Holds the current value (never null). */ private T value; /** * Holds the default value (never null). */ private T defaultValue; /** * Holds the class where this configurable is defined. */ private Class<?> container; /** * Holds the min value for optimization. */ private T min; /** * Holds the max value for optimization. */ private T max; /** * Creates a new configurable having the specified default value. * * @param defaultValue * the default value. * @throws IllegalArgumentException * if <code>defaultValue</code> is <code>null</code>. */ public Configurable(String name, T defaultValue) { init(name, defaultValue, null, null); } /** * Creates a new configurable having the specified default value. * * @param defaultValue * the default value. * @throws IllegalArgumentException * if <code>defaultValue</code> is <code>null</code>. */ public Configurable(String name, T defaultValue, T min, T max) { if (min == null) { throw new IllegalArgumentException("Min value cannot be null"); } if (max == null) { throw new IllegalArgumentException("Max value cannot be null"); } init(name, defaultValue, min, max); } private void init(String name, T defaultValue, T min, T max) { if (name == null) { throw new IllegalArgumentException("Name cannot be null"); } if (defaultValue == null) { throw new IllegalArgumentException("Default value cannot be null"); } this.container = findContainer(); this.name = new StringBuilder(container.getName()).append('#').append(name.toUpperCase()).toString(); this.defaultValue = defaultValue; this.value = defaultValue; this.min = min; this.max = max; configurables.put(this.name, this); Object value = configuration.get(this.name); if (value != null) { Configurable.configure(this, Util.coerceType(value, defaultValue.getClass())); } } /** * Returns the current value for this configurable. * * @return the current value (always different from <code>null</code>). */ public T get() { return value; } /** * Returns the default value for this configurable. * * @return the default value (always different from <code>null</code>). */ public T getDefault() { return defaultValue; } public T getMin() { return min; } public T getMax() { return max; } /** * Returns the container class of this configurable. * * @return the container class */ public Class<?> getContainer() { return container; } /** * Returns the field name of this configurable * @return this configurable name */ public String getName() { return name; } public boolean isOptimizable() { return defaultValue instanceof Number && min != max; } /** * Notifies this configurable that its runtime value is going to be changed. * The default implementation does nothing. * * @param oldValue * the previous value. * @param newValue * the new value. * @throws UnsupportedOperationException * if dynamic reconfiguration of this configurable is not allowed * (regardless of the security context). */ protected void notifyChange(T oldValue, T newValue) throws java.lang.UnsupportedOperationException { } /** * Returns the string representation of the value of this configurable. * * @return <code>String.valueOf(this.get())</code> */ @Override public String toString() { return String.valueOf(value); } private static Class<?> findContainer() { try { StackTraceElement[] stack = new Throwable().getStackTrace(); String className = stack[3].getClassName(); int sep = className.indexOf("$"); if (sep >= 0) { // If inner class, remove suffix. className = className.substring(0, sep); } return Class.forName(className); } catch (IllegalArgumentException e) { throw e; } catch (Exception e) { throw new IllegalArgumentException(e.getMessage(), e); } } public static Configurable<?> getInstance(String name, Object obj) { Field field = Util.getField(obj.getClass(), name); if (field == null) { logger.warn("Configurable {} not found", name); return null; } try { return (Configurable<?>) field.get(obj); } catch (Exception e) { logger.error(e.getMessage(), e); return null; } } /** * Sets the run-time value of the specified configurable. If the configurable * value is different from the previous one, then {@link #notifyChange} is * called. * * @param configurable * the configurable being configured. * @param object * the new run-time value. * @throws IllegalArgumentException * if <code>value</code> is <code>null</code>. */ @SuppressWarnings("unchecked") public static <T> void configure(Configurable<T> configurable, Object object) throws SecurityException { if (object == null) { throw new IllegalArgumentException("New value cannot be null"); } Object oldValue = configurable.value; if (!object.equals(oldValue)) { logger.info("{} set to {}", configurable.getName(), object); configurable.value = (T) object; configurable.notifyChange((T) oldValue, (T) object); } } /** * Convenience method to read the specified properties and reconfigure * accordingly. * * @param map * the properties. */ public static void configure(Map<?, ?> map) { for (Map.Entry<?, ?> e : map.entrySet()) { Configurable<?> c = configurables.get((String) e.getKey()); if (c != null) { Configurable.configure(c, Util.coerceType(e.getValue(), c.getDefault().getClass())); } } configuration.putAll(map); } public static void configure(Object obj, String name, Object value) { Configurable<?> c = getInstance(name, obj); if (c != null) { Configurable.configure(c, Util.coerceType(value, c.getDefault().getClass())); } } public static void configure(String name, Object value) { for (Configurable<?> c : configurables.values()) { if (name.equals(c.getName())) { Configurable.configure(c, Util.coerceType(value, c.getDefault().getClass())); } } configuration.put(name, value); } public static void copyConfigurables(Object src, Object dst) { Map<String, Configurable<?>> dstConfigurables = getConfigurables(dst); for (Configurable<?> srcConf : getConfigurables(src).values()) { Configurable<?> dstConf = dstConfigurables.get(srcConf.getName()); if (dstConf != null) { Configurable.configure(dstConf, srcConf.get()); } } } public static List<Configurable<?>> getOptimizableConfigurables(Object obj) { List<Configurable<?>> configurables = new ArrayList<Configurable<?>>(); for (Configurable<?> c : getConfigurables(obj).values()) { if (c.isOptimizable()) { configurables.add(c); } } return configurables; } public static Map<String, Configurable<?>> getConfigurables(Object obj) { try { Collection<Field> fields = Util.getFields(obj.getClass()); Map<String, Configurable<?>> configurables = new TreeMap<String, Configurable<?>>(); for (Field f : fields) { if (Configurable.class.isAssignableFrom(f.getType())) { Configurable<?> c = (Configurable<?>) f.get(obj); configurables.put(f.getName(), c); } } return configurables; } catch (IllegalAccessException e) { throw new IllegalArgumentException(e); } } public static Map<String, Object> getConfiguration(Object obj) { try { Collection<Field> fields = Util.getFields(obj.getClass()); Map<String, Object> configurables = new TreeMap<String, Object>(); for (Field f : fields) { if (f.getType().isAssignableFrom(Configurable.class)) { Configurable<?> c = (Configurable<?>) f.get(obj); configurables.put(f.getName(), c.get()); } } return configurables; } catch (IllegalAccessException e) { throw new IllegalArgumentException(e); } } public static Properties getConfiguration() { return configuration; } private static Properties loadProperties(String filename, Properties defaults) throws IOException { Properties properties = new Properties(defaults); InputStream is = null; try { is = ClassLoader.getSystemClassLoader().getResourceAsStream(filename); if (is == null) { try { is = new FileInputStream(filename); } catch (Exception e) { } } if (is != null) { properties.load(is); logger.info("Configuration read from '{}'", filename); if (logger.isDebugEnabled()) { logger.debug(properties.toString()); } } } finally { try { if (is != null) { is.close(); } } catch (IOException e) { } } return properties; } private static final String systemPropertyName = "jtrade.config"; static { String filename = null; try { filename = System.getProperty(systemPropertyName, null); if (filename == null) { filename = "/jtrade-" + InetAddress.getLocalHost().getHostName().toLowerCase() + ".properties"; } configuration = loadProperties(filename, loadProperties("/jtrade.properties", null)); configure(configuration); } catch (Throwable t) { throw new IllegalStateException(String.format("Could not read configuration from '%s': %s", filename, t.getMessage()), t); } } }