/** * VMware Continuent Tungsten Replicator * Copyright (C) 2015 VMware, Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Initial developer(s): Robert Hodges * Contributor(s): Gilles Rayrat */ package com.continuent.tungsten.common.config; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.io.Serializable; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; import java.util.TreeMap; import java.util.TreeSet; import javax.xml.bind.annotation.XmlRootElement; import org.apache.log4j.Logger; import org.codehaus.jackson.JsonGenerationException; import org.codehaus.jackson.JsonParseException; import org.codehaus.jackson.annotate.JsonAnyGetter; import org.codehaus.jackson.annotate.JsonAnySetter; import org.codehaus.jackson.annotate.JsonIgnore; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.map.JsonMappingException; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.ObjectWriter; import org.codehaus.jackson.map.SerializationConfig.Feature; import org.codehaus.jackson.map.annotate.JsonSerialize; /** * Defines a simple HashMap wrapper that can be used to store and retrieve * properties using typed getters and setters. There is support for serializing * to and from Java properties format, setting variables, merging properties, * and other niceties. * * @author <a href="mailto:robert.hodges@continuent.com">Robert Hodges</a> * @version 1.0 */ @XmlRootElement @JsonIgnoreProperties(ignoreUnknown = true) @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) public class TungstenProperties implements Serializable { private static final long serialVersionUID = 1; private static Logger logger = Logger.getLogger(TungstenProperties.class); public static final String ENDOFPROPERTIES_TAG = "#EOF"; public static final String ENDOFLINE_TAG = "#EOL"; private static final String MAP_KEY_SEPARATOR = "#TP_KEY#"; enum ParseState { NONE, DOLLAR, LBRACKET, NAME }; @JsonIgnore protected Map<String, Object> properties; private boolean sorted; @JsonIgnore private boolean beanSupportEnabled = false; // --- Support for "flat" JSON --- /* * This allows the Jackson JSON serialisation to "flatten" the properties * with the rest of the atributes */ // "any getter" needed for serialization @JsonAnyGetter public Map<String, Object> any() { return properties; } @JsonAnySetter public void set(String name, Object value) { properties.put(name, value); } // ------------------------------ /** * Creates a new instance. */ public TungstenProperties() { this(false); } /** * Creates a new instance from an existing map. */ public TungstenProperties(Map<String, String> map) { properties = new HashMap<String, Object>(map); } /** * Creates a new instance with sorting. */ public TungstenProperties(boolean sorted) { properties = new HashMap<String, Object>(); this.sorted = sorted; } /** * Returns true if this instance supports extracting/setting bean * properties. */ public boolean isBeanSupportEnabled() { return beanSupportEnabled; } /** * If set to true this instance will support extracting/setting bean * properties. * * @param beanSupportEnabled */ public void setBeanSupportEnabled(boolean beanSupportEnabled) { this.beanSupportEnabled = beanSupportEnabled; } /** * Loads values from Java properties file format with variable * substitutions. */ public void load(InputStream is) throws IOException { load(is, true); } /** * Loads values from Java properties file format. Current values are * obliterated. * * @param is InputStream containing properties. * @param doSubstitutions If true perform variable substitutions */ public void load(InputStream is, boolean doSubstitutions) throws IOException { // Load the properties file. Properties props = new Properties(); props.load(is); if (doSubstitutions) substituteSystemValues(props); load(props); /* * props ends up holding a reference to the InputStream. So even * though the Properties instance should be eligible for collection on * method exit, give GC a hand and null out the props here. */ props = null; } /** * Loads values from a string of name-value pairs of the form * a=1;b=2;...;z=N. White space is ignored. */ public void load(String nameValuePairs, boolean doSubstitutions) { Properties props = new Properties(); boolean parsingKey = true; int index = 0; StringBuffer keyBuf = new StringBuffer(); StringBuffer valueBuf = new StringBuffer(); while (index < nameValuePairs.length()) { char next = nameValuePairs.charAt(index++); if (parsingKey && next == '=') { parsingKey = false; } else if (parsingKey) { keyBuf.append(next); } else if (!parsingKey && next == ';') { // At end of name/value pair, so insert values. String key = keyBuf.toString().trim(); String value = valueBuf.toString().trim(); props.setProperty(key, value); parsingKey = true; keyBuf = new StringBuffer(); valueBuf = new StringBuffer(); } else { valueBuf.append(next); } } // If there is a left over name/value pair, append. String key = keyBuf.toString().trim(); String value = valueBuf.toString().trim(); if (key.length() > 0 && value.length() > 0) props.setProperty(key, value); // Perform substitutions if desired, then load properties. if (doSubstitutions) substituteSystemValues(props); load(props); } /** * Load values from a JSON serialized string * * @param json The JSON serialized string * @throws JsonParseException * @throws JsonMappingException * @throws IOException */ public static TungstenProperties loadFromJSON(String json) throws JsonParseException, JsonMappingException, IOException { ObjectMapper mapper = new ObjectMapper(); TungstenProperties tungstenProp = mapper.readValue(json, TungstenProperties.class); return tungstenProp; } /** * Get an instances of Properties that is populated by the current values of * the instance on which it's called. * * @return Properties */ public Properties getProperties() { Properties props = new Properties(); props.putAll(properties); return props; } /** * Load values from a Properties instance. Current values are obliterated. */ public void load(Properties props) { HashMap<String, Object> map = new HashMap<String, Object>(); Enumeration<?> keys = props.propertyNames(); while (keys.hasMoreElements()) { String key = (String) keys.nextElement(); map.put(key, props.getProperty(key)); } properties = map; } /** * Substitute system values up to a certain number of times. This permits * clients to reuse variables to get multiple substitutions without running * into problems with infinite loops. */ public static int substituteSystemValues(Properties props, int iterations) { int substitutions = 0; // Substitute until we exhaust either iterations or substitutions. for (int i = 0; i < iterations; i++) { int count = substituteSystemValues(props); if (count == 0) break; else substitutions += count; } return substitutions; } /** * Scan Properties instance values replacing any expression of the form * ${name} where 'name' is a key in System.properties *or* a local property * in the same file, where local property substitutions take priority over * system property names. If there is no such value the expression is left * as is. */ public static int substituteSystemValues(Properties props) { int substitutions = 0; // Make a copy of the properties object for local variable // substitutions. Properties originalProps = new Properties(); originalProps.putAll(props); // Perform substitutions. Enumeration<Object> en = props.keys(); while (en.hasMoreElements()) { String key = (String) en.nextElement(); String value = props.getProperty(key); if (value == null) continue; StringBuffer newValue = new StringBuffer(); StringBuffer expression = null; StringBuffer name = null; // Execute a simple state machine to find and resolve // property name expressions. ParseState state = ParseState.NONE; for (int i = 0; i < value.length(); i++) { char c = value.charAt(i); switch (state) { case NONE : // Look for a $ indicating start of expression. if (c == '$') { state = ParseState.DOLLAR; expression = new StringBuffer(); expression.append(c); } else newValue.append(c); break; case DOLLAR : // Look for a left bracket. expression.append(c); if (c == '{') { state = ParseState.LBRACKET; } else { state = ParseState.NONE; newValue.append(expression.toString()); expression = null; } break; case LBRACKET : // Look for the start of the properties name. expression.append(c); if (Character.isLetterOrDigit(c)) { state = ParseState.NAME; name = new StringBuffer(); name.append(c); } else { state = ParseState.NONE; newValue.append(expression.toString()); expression = null; } break; case NAME : // Accumulate the properties name to right bracket. expression.append(c); if (c == '}') { // Try to translate first a local property then a // system property. String embeddedKey = name.toString(); if (embeddedKey.length() > 0) { String originalValue = originalProps .getProperty(embeddedKey); String systemValue = System .getProperty(embeddedKey); if (originalValue != null) { expression = new StringBuffer(originalValue); substitutions++; } else if (systemValue != null) { expression = new StringBuffer(systemValue); substitutions++; } } name = null; state = ParseState.NONE; newValue.append(expression.toString()); expression = null; } else { name.append(c); } break; } } // If we still have an expression value left over, we need to apply // it to the new value. if (expression != null) { newValue.append(expression); } // Finally, set the new value. props.setProperty(key, newValue.toString()); } // Return the total number of substitutions. return substitutions; } /** * Stores values in Java properties file format. This does not work for * embedded lists. */ @SuppressWarnings("serial") public void store(OutputStream os) throws IOException { Properties props = null; if (sorted) { props = new Properties() { @Override public Set<Object> keySet() { return Collections.unmodifiableSet(new TreeSet<Object>( super.keySet())); } @Override public synchronized Enumeration<Object> keys() { return new Enumeration<Object>() { private Iterator<Object> iterator; { iterator = keySet().iterator(); } public boolean hasMoreElements() { return iterator.hasNext(); } public Object nextElement() { return iterator.next(); } }; } }; } else { props = new Properties(); } for (String key : properties.keySet()) { if (this.getString(key) != null) props.setProperty(key, this.getString(key)); } props.store(os, "Tungsten properties"); } /** * Applies the current properties to the given object, stopping and throwing * errors if a property has no matching setter in the given object. This is * equivalent to <code>applyProperties(o, false)</code> * * @param o instance on which to set properties */ public void applyProperties(Object o) { applyProperties(o, false); } /** * Applies the properties in the TungstenProperties object to corresponding * properties on a Java object by matching property names to setter methods * on the object. Here are the rules for matching. * <ol> * <li>The first letter of the property is capitalized, so foo_bar becomes * Foo_bar.</li> * <li>If an underscore ("_") occurs in the property name, it is omitted and * the following character, if any, is capitalized. Foo_bar becomes FooBar.</li> * <li>The prefix "set" is added to the result. FooBar becomes setFooBar. * </ol> * The setter method, if found, must have a single argument and must be * publicly accessible. Multiple setters that differ only by argument type * are not supported. * * @param o Instance for which we are to set properties * @param ignoreIfMissing whether or not stop and throw an error if a * property has no matching setting in the given object instance * @throws PropertyException Thrown if we cannot find a setter for a * property or if invocation of the setter fails */ public void applyProperties(Object o, boolean ignoreIfMissing) { // Find methods on this instance. Method[] methods = o.getClass().getMethods(); // Allocated maps to hold data for Java bean classes (i.e., embedded // objects). Map<String, TungstenProperties> beanMaps = new HashMap<String, TungstenProperties>(); // Extract all property names that have "." in them. These are // properties that apply to embedded Java Beans. The values need to go // into a map for each bean. for (String key : keyNames()) { // If key has a period in it followed by a suffix, it is a bean // property. int period = key.indexOf('.'); if (period == -1 || (period + 1) >= key.length()) continue; // Determine the prefix and find the corresponding value map. Make // a new one if it is not there. String prefix = key.substring(0, period); TungstenProperties beanProps = beanMaps.get(prefix); if (beanProps == null) { beanProps = new TungstenProperties(); beanMaps.put(prefix, beanProps); } // Add the property value to the bean properties. beanProps.put(key.substring(period + 1), getString(key)); } // Try to find and invoke setter method corresponding to each property. for (String key : keyNames()) { // Skip keys for embedded beans, i.e, those with dots. They will be // addressed later. int period = key.indexOf('.'); if (period > 1 && (period + 1) < key.length()) continue; // Construct setter name. StringBuffer setterNameBuffer = new StringBuffer(); setterNameBuffer.append("set"); char prev = '\0'; for (int i = 0; i < key.length(); i++) { char c = key.charAt(i); if (i == 0) { // Upper case first character. setterNameBuffer.append(Character.toUpperCase(c)); } else if (prev == '\0') { // Append ordinary character unless it's an underscore. if (c == '_') prev = c; else setterNameBuffer.append(c); } else { // Upper case character following an underscore. setterNameBuffer.append(Character.toUpperCase(c)); prev = '\0'; } } // Don't forget to add a trailing underscore. if (prev != '\0') setterNameBuffer.append(prev); String setterName = setterNameBuffer.toString(); // Find a setter on the instance class if it exists. Method setter = null; for (Method m : methods) { if (!m.getName().equals(setterName)) continue; else if (m.getParameterTypes().length != 1) continue; else { setter = m; break; } } if (setter == null) { if (ignoreIfMissing) { if (logger.isDebugEnabled()) logger.debug("Ignoring missing setter for property=" + key); continue; } throw new PropertyException( "Unable to find method corresponding to property: " + " class=" + o.getClass().getName() + " property=" + key + " expected setter=" + setterName); } // Construct the argument. String value = getString(key); if (value == null) continue; Object arg = null; // Next two lines generate Eclipse warnings. Class<?>[] argTypes = setter.getParameterTypes(); Class<?> arg0Type = argTypes[0]; // Handle primitive types if (arg0Type.isPrimitive()) { try { if (arg0Type == Integer.TYPE) arg = new Integer(value); else if (arg0Type == Long.TYPE) arg = new Long(value); else if (arg0Type == Boolean.TYPE) arg = new Boolean(value); else if (arg0Type == Character.TYPE) arg = new Character(value.charAt(0)); else if (arg0Type == Float.TYPE) arg = new Float(value); else if (arg0Type == Double.TYPE) arg = new Double(value); else if (arg0Type == Byte.TYPE) arg = new Byte(value); else if (arg0Type == Short.TYPE) arg = new Short(value); } catch (Exception e) { throw new PropertyException( "Unable to translate property value: key=" + key + " value = " + value, e); } } // Special storage methods: else if (arg0Type == Date.class) { try { // Date type is stored as a long provided by // java.util.Date#getTime() arg = new Date(new Long(value)); } catch (Exception e) { throw new PropertyException( "Unable to translate property value: key=" + key + " value = " + value, e); } } else if (arg0Type == List.class) { arg = Arrays.asList(value.split(",")); } else { // For other types, try three methods: // 1. Try to treat it as a bean. arg = constructBean(value, beanMaps.get(key), ignoreIfMissing); // 2. Try to call Constructor(String) if (arg == null) arg = constructFromString(arg0Type, value); // 3. Try to call <Type>.valueOf(String) if (arg == null) arg = constructUsingValueOf(arg0Type, value); // At this point we are done and need to give up. if (arg == null) { if (ignoreIfMissing) { continue; } logger.warn("Could not instantiate non-bean arg of type " + arg0Type + ". No Constructor(String) nor valueOf(String) found in this class"); throw new PropertyException( "Unsupported property type: key=" + key + " type=" + arg0Type + " value=" + value); } } // Now set the value. try { setter.invoke(o, new Object[]{arg}); if (logger.isDebugEnabled() == true) { logger.debug("Set attribute in object=<" + o.getClass().getSimpleName() + "> from key <" + key + ">"); } } catch (Exception e) { throw new PropertyException("Unable to set property: key=" + key + " value = " + value, e); } } } // Try to instantiate and configure a bean. private Object constructBean(String className, TungstenProperties beanProps, boolean ignoreIfMissing) { // Return null if we don't support beans. if (!this.isBeanSupportEnabled()) return null; // Load the class. Class<?> type = null; try { type = Class.forName(className); } catch (Exception e) { if (logger.isDebugEnabled()) { logger.debug("Unable to load bean class: name=" + className, e); } return null; } // If this does not look like a bean, just return. if (!isBean(type)) { if (logger.isDebugEnabled()) { logger.debug("Class does not meet bean qualifications: name=" + className); } return null; } // Try to instantiate. Object arg = null; try { arg = type.newInstance(); } catch (Exception e) { if (logger.isDebugEnabled()) { logger.debug( "Unable to instantiate class using default constructor: name=" + type.getName(), e); } return null; } // Assign values. if (beanProps != null) beanProps.applyProperties(arg, ignoreIfMissing); return arg; } // Try to instantiate a type using the String constructor. private Object constructFromString(Class<?> type, String value) { try { Object arg = type.getConstructor(String.class).newInstance(value); if (logger.isTraceEnabled()) logger.trace("String constructor for arg type " + type + " found. arg value is " + value); return arg; } catch (Exception e) { if (logger.isDebugEnabled()) { logger.debug( "Unable to instantiate class using string constructor: name=" + type.getName() + " value=" + value, e); } return null; } } // Try to instantiate a type using valueOf() method. private Object constructUsingValueOf(Class<?> type, String value) { try { Object arg = type.getMethod("valueOf", new Class[]{String.class}) .invoke(type, value); if (logger.isTraceEnabled()) logger.trace("Method valueOf(String) for arg type " + type + " found. arg value is " + arg); return arg; } catch (Exception e) { if (logger.isDebugEnabled()) { logger.debug("No valueOf(String) method found for arg type " + type, e); } return null; } } /** * Create a set of initialized properties from the set of fields in a * specific object instance. * * @param o * @param ignoreIfUnsupported */ @SuppressWarnings("unchecked") public void extractProperties(Object o, boolean ignoreIfUnsupported) { if (logger.isDebugEnabled()) logger.debug("Extracting properties from object=" + o.getClass().getName()); Field[] fields = o.getClass().getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); if (logger.isDebugEnabled()) logger.debug("Extracting field=" + field.getName()); try { if (field.getType() == Integer.TYPE) { this.setInt(field.getName(), field.getInt(o)); } else if (field.getType() == Long.TYPE) { this.setInt(field.getName(), (int) field.getLong(o)); } else if (field.getType() == Boolean.TYPE) { this.setBoolean(field.getName(), field.getBoolean(o)); } else if (field.getType() == String.class) { this.setString(field.getName(), (String) field.get(o)); } else if (field.getType() == Float.TYPE) { this.setFloat(field.getName(), (Float) field.get(o)); } else if (field.getType() == Double.TYPE) { this.setDouble(field.getName(), (Double) field.get(o)); } else if (field.getType() == Date.class) { this.setDate(field.getName(), (Date) field.get(o)); } else if (field.getType() == List.class) { this.setStringList(field.getName(), (List<String>) field.get(o)); } else { // If we have a type that follows bean conventions, try to // extract its properties. Object bean = field.get(o); if (bean == null) { if (logger.isDebugEnabled()) { logger.debug("Skipping property with null value, prop=" + field.getName()); } } else if (isBean(bean.getClass())) { // Extract bean properties. TungstenProperties beanProps = new TungstenProperties(); beanProps.extractProperties(bean, ignoreIfUnsupported); // Write the bean class name. String beanKey = field.getName(); setString(beanKey, bean.getClass().getName()); // Write the properties of the bean adding the bean key // as a prefix. this.putAllWithPrefix(beanProps, beanKey + "."); } else if (ignoreIfUnsupported) { if (logger.isDebugEnabled()) { logger.debug("Skipping property with unsupported type, prop=" + field.getName()); } continue; } else { throw new PropertyException( "Unsupported property type:" + field.getType()); } } } catch (IllegalAccessException i) { logger.error("Exception while trying to extract values from field=" + field.getName() + " of class=" + o.getClass().getName()); } } } /** * Trims all property values to remove leading and trailing whitespace. */ public void trim() { for (String key : keyNames()) { String value = (String) properties.get(key); if (value != null) properties.put(key, value.trim()); } } /** * Removes the property indicated by the key, if it exists, and returns the * value. */ public String remove(String key) { String value = get(key); properties.remove(key); return value; } /** * Clears all property values so that the underlying map is empty. */ public void clear() { properties.clear(); } /** * Returns true if properties map is empty. */ @JsonIgnore public boolean isEmpty() { return properties.isEmpty(); } /** * Returns keys of all properties currently stored in this instance. */ public Set<String> keyNames() { return properties.keySet(); } /** * Returns keys of all properties where the key name matches the provided * prefix. */ public Set<String> keyNames(String prefix) { Set<String> keys = keyNames(); HashSet<String> subset = new HashSet<String>(); for (String key : keys) { if (key != null && key.startsWith(prefix)) subset.add(key); } return subset; } /** * Returns true if the indicated property key exists. This method returns * true even if the property is set to null. */ public boolean containsKey(String key) { return (properties.containsKey(key)); } /** * Returns the number of properties in this instance. */ public int size() { return properties.size(); } /** * Merges the properties provided as an argument with these properties, * overriding current values wherever there are overlaps. */ public void putAll(TungstenProperties props) { properties.putAll(props.map()); } /** * Prefixes all the given properties with the given string and merges this * new set with the actual ones, overriding current values with same keys if * any */ public void putAllWithPrefix(TungstenProperties props, String prefix) { TungstenProperties newProps = new TungstenProperties(); if (props.isEmpty()) { newProps.setObject(prefix, props); } else { for (String key : props.keyNames()) { newProps.setObject(prefix + key, props.getObject(key)); } } putAll(newProps); } /** * Sets the value as a string. */ public void setString(String key, String value) { properties.put(key, value); } /** * Utility method to help with compatibility with Properties * * @param key * @param value */ public void setProperty(String key, String value) { setString(key, value); } public void put(Object key, Object value) { setObject((String) key, value); } /** * Utility method to help with compatibility with Properties * * @param key * @param value */ public void put(String key, String value) { setString(key, value); } /** * Sets the property value from an object using its toString() method. */ public void setObject(String key, Object value) { if (value == null) properties.put(key, null); else properties.put(key, value); } public void setInt(String key, int value) { properties.put(key, Integer.toString(value)); } public void setLong(String key, long value) { properties.put(key, Long.toString(value)); } public void setFloat(String key, float value) { properties.put(key, Float.toString(value)); } public void setDouble(String key, double value) { properties.put(key, Double.toString(value)); } public void setBoolean(String key, boolean value) { properties.put(key, Boolean.toString(value)); } public void setFile(String key, File value) { properties.put(key, value.toString()); } /** * Date type is stored as a long representing the number of milliseconds * since January 1, 1970, 00:00:00 GMT as retrieved by * {@link Date#getTime()} * * @param key unique identifier for this property * @param value the date to store */ public void setDate(String key, Date value) { if (value != null) setLong(key, value.getTime()); } /** * Sets a value from a String list. This results in a series of * comma-separated values that can be read with * {@link #getStringList(String)}. */ public void setStringList(String key, List<String> list) { StringBuffer sb = new StringBuffer(); if (list == null) { setString(key, null); } else { for (String value : list) { if (sb.length() > 0) sb.append(","); sb.append(value); } setString(key, sb.toString()); } } /** * Stores a time interval provided in milliseconds, which is the base * representation. */ public void setInterval(String key, Long value) { setLong(key, value); } /** * Stores a time interval provided as an object by converting to an interval * instance. */ public void setInterval(String key, Interval value) { setLong(key, value.longValue()); } /** * Stores a TungstenProperties as a property * * @param key the key to identify the property * @param tungstenProperties the TungstenProperties to store */ public void setTungstenProperties(String key, TungstenProperties tungstenProperties) { properties.put(key, tungstenProperties); } /** * Stores a Map which keys are strings and values are TungstenProperties. * This function is meant to be used to store a data source map * <p> * Each TungstenProperties entry in the given map will be stored in this * object with its keys prefixed by the corresponding map key + its own key, * separated by MAP_KEY_SEPARATOR * <p> * Note that none of the TungstenProperties in the given map can be null * * @param map the map to store */ public void setDataSourceMap(Map<String, TungstenProperties> map) { for (String key : map.keySet()) { putAllWithPrefix(map.get(key), key + MAP_KEY_SEPARATOR); } } /** * Stores a Map which keys are strings and values are Maps of * String/TungstenProperties. This function is meant to be used to store a * cluster map * <p> * Each entry in the given map will be stored in this object with its keys * prefixed by the corresponding map key + MAP_KEY_SEPARATOR + its own key + * MAP_KEY_SEPARATOR + its Tungsten properties key. * <p> * Note that none of the TungstenProperties in the given map can be null * * @param map the map to store */ public void setClusterMap(Map<String, Map<String, TungstenProperties>> map) { for (String key : map.keySet()) { Map<String, TungstenProperties> val = map.get(key); for (String valKey : val.keySet()) { putAllWithPrefix(val.get(valKey), key + MAP_KEY_SEPARATOR + valKey + MAP_KEY_SEPARATOR); } } } /** * Given a key, gets the object * * @param key * @param defaultValue * @param required * @return Object */ public Object getObject(String key, Object defaultValue, boolean required) { // return (String) getObject(key, defaultValue, required); Object value = properties.get(key); if (value != null) return value; else if (defaultValue != null) return defaultValue; if (required) throw new PropertyException( "No value found for required property: " + key); else return null; } /** * Returns the value as a String with an optional default value and checking * to ensure value is present if required * * @param key The name of the property * @param defaultValue An optional default value * @param required If true, a value or default must be present * @return The corresponding value or null if not found and no default * exists * @throws PropertyException if the value is required but does not exist */ public String getString(String key, String defaultValue, boolean required) { Object o = getObject(key, defaultValue, required); if (o != null) return o.toString(); return null; } /** * Returns the value as a String or null if not found */ public String getString(String key) { return getString(key, null, false); } public String getProperty(String key, String defaultValue) { return getString(key, defaultValue, false); } public String getProperty(String key) { return getString(key); } public String get(String key) { return getString(key); } public Object getObject(String key) { return getObject(key, null, false); } public int getInt(String key) { return getInt(key, null, false); } public int getInt(String key, String defaultValue, boolean required) { return Integer.parseInt(getString(key, defaultValue, required)); } public long getLong(String key) { return getLong(key, null, false); } public long getLong(String key, String defaultValue, boolean required) { return Long.parseLong(getString(key, defaultValue, required)); } public float getFloat(String key) { return getFloat(key, null, false); } public float getFloat(String key, String defaultValue, boolean required) { return Float.parseFloat(getString(key, defaultValue, required)); } public double getDouble(String key) { return getDouble(key, null, false); } public double getDouble(String key, String defaultValue, boolean required) { return Double.parseDouble(getString(key, defaultValue, required)); } public boolean getBoolean(String key) { return getBoolean(key, null, false); } public boolean getBoolean(String key, String defaultValue, boolean required) { return Boolean.parseBoolean(getString(key, defaultValue, required)); } public File getFile(String key) { return new File(getString(key)); } public File getFile(String key, String defaultValue, boolean required) { return new File(getString(key, defaultValue, required)); } public Date getDate(String key, String defaultValue, boolean required) { return new Date(Long.parseLong(getString(key, defaultValue, required))); } public Date getDate(String key) { return getDate(key, null, false); } /** * Returns a list of strings from a value containing a list of string values * separated by commas or whitespace characters. Here are some examples: * <ul> * <li>"a,b,c" returns "a", "b", "c"</li> * <li>" a b c, " returns "a", "b", "c"</li> * <li>"," returns an empty list * <li>"I think so but, it's not!" returns "I", "think", "so", "but", * "it's", and "not!" * </ul> */ public List<String> getStringList(String key) { List<String> list = new ArrayList<String>(); String listValues = getString(key); if (listValues == null) return list; else if (listValues instanceof String) { StringTokenizer st = new StringTokenizer(listValues, ", \t\n\r\f"); while (st.hasMoreTokens()) list.add(st.nextToken()); return list; } else { throw new PropertyException( "Invalid type for comma-separated list: " + listValues.getClass().getName()); } } /** * Returns an interval or the default value. */ public Interval getInterval(String key, String defaultValue, boolean required) { return new Interval(getString(key, defaultValue, required)); } /** * Returns an interval value. */ public Interval getInterval(String key) { return getInterval(key, null, false); } /** * Returns a TungstenProperties value. * * @param key identifying the property * @return TungstenProperties */ @SuppressWarnings("unchecked") public TungstenProperties getTungstenProperties(String key) { TungstenProperties tungstenProp = null; Object value = this.getObject(key); if (value instanceof TungstenProperties) tungstenProp = (TungstenProperties) value; else if (value instanceof LinkedHashMap<?, ?>) tungstenProp = new TungstenProperties((Map<String, String>) value); return tungstenProp; } /** * Retrieves a data source map as stored by {@link #setDataSourceMap(Map)} * * @return a String/TungstenProperties map */ @JsonIgnore public Map<String, TungstenProperties> getDataSourceMap() { Map<String, TungstenProperties> result = new HashMap<String, TungstenProperties>(); Set<String> keys = keyNames(); while (!keys.isEmpty()) { String key = keys.iterator().next(); String realKey = key.substring(0, key.indexOf(MAP_KEY_SEPARATOR)); result.put(realKey, subset(realKey + MAP_KEY_SEPARATOR, true, true)); keys = keyNames(); } return result; } /** * Retrieves a cluster map as stored by {@link #setClusterMap(Map)} * * @return a String/(String/TungstenProperties) map */ @JsonIgnore public Map<String, Map<String, TungstenProperties>> getClusterMap() { Map<String, Map<String, TungstenProperties>> fullResult = new HashMap<String, Map<String, TungstenProperties>>(); Set<String> keys = keyNames(); while (!keys.isEmpty()) { String key = keys.iterator().next(); int mapKeyIdx = key.indexOf(MAP_KEY_SEPARATOR); if (mapKeyIdx <= 0) { // not a cluster map entry, ignore it keys.remove(key); continue; } String serviceKey = key.substring(0, mapKeyIdx); Map<String, TungstenProperties> result = fullResult.get(serviceKey); if (result == null) { result = new HashMap<String, TungstenProperties>(); } String valKey = key.substring(key.indexOf(MAP_KEY_SEPARATOR) + MAP_KEY_SEPARATOR.length(), key.lastIndexOf(MAP_KEY_SEPARATOR)); result.put( valKey, subset(serviceKey + MAP_KEY_SEPARATOR + valKey + MAP_KEY_SEPARATOR, true, true)); fullResult.put(serviceKey, result); keys = keyNames(); } return fullResult; } /** * Returns a shallow copy of the underlying map as a generic Map. */ public Map<String, String> map() { return hashMap(); } /** * Returns a shallow copy of the underlying map as a HashMap. */ public HashMap<String, String> hashMap() { HashMap<String, String> retMap = new HashMap<String, String>(); for (String key : properties.keySet()) { Object value = properties.get(key); if (value != null) { retMap.put(key, value.toString()); } else { retMap.put(key, null); } } return retMap; } /** * Returns values as a string of name/value pairs that can be loaded using * {@link #load(String, boolean)}. */ public String toNameValuePairs() { StringBuffer pairs = new StringBuffer(); for (String key : properties.keySet()) { if (pairs.length() > 0) pairs.append(';'); pairs.append(key).append("=").append(properties.get(key)); } return pairs.toString(); } /** * Returns a TungstenProperties instance consisting of the property names * that match the given prefix * * @param prefix Return only those properties that match the prefix * @param removePrefix If true remove the prefix from each property name */ public TungstenProperties subset(String prefix, boolean removePrefix) { return subset(prefix, removePrefix, false); } /** * Returns a TungstenProperties instance consisting of the property names * that match the given prefix * * @param prefix Return only those properties that match the prefix * @param removePrefix If true remove the prefix from each property name * @param removeProps If true remove the matching key/value pairs from these * properties */ public TungstenProperties subset(String prefix, boolean removePrefix, boolean removeProps) { TungstenProperties tp = new TungstenProperties(); Set<String> prefixKeys = keyNames(prefix); int nameIndex = 0; if (removePrefix) nameIndex = prefix.length(); for (String key : prefixKeys) { String newKey = key.substring(nameIndex); if (newKey.length() > 0) tp.setObject(newKey, getObject(key)); if (removeProps) remove(key); } return tp; } /** * Returns true if the argument contains exactly the same property values as * this properties instance. */ public boolean equals(Object o) { if (!(o instanceof TungstenProperties)) return false; Map<String, Object> tp2 = ((TungstenProperties) o).properties; return properties.equals(tp2); } /** * Returns toString of underlying properties rather than wrapper. */ public String toString() { StringBuilder builder = new StringBuilder(); TreeMap<String, Object> orderedProps = new TreeMap<String, Object>(); orderedProps.putAll(properties); builder.append("\n{\n"); int propCount = 0; for (String key : orderedProps.keySet()) { Object value = orderedProps.get(key); // Skip processing null values... if (value == null) { continue; } if (++propCount > 1) builder.append("\n"); builder.append(" ").append(key).append("="); if (value instanceof String) { // Strings must properly escape control characters. String valueAsString = (String) value; for (int i = 0; i < valueAsString.length(); i++) { char c = valueAsString.charAt(i); if (Character.isISOControl(c)) { // Print Unicode escape sequence. int cAsInt = c; String escapedValue = String.format("\\u%04x", cAsInt); builder.append(escapedValue); } else { // Otherwise just print the character representation. builder.append(c); } } } else { builder.append(value.toString()); } } builder.append("\n}"); return builder.toString(); } public String toJSON() throws JsonGenerationException, JsonMappingException, IOException { return this.toJSON(false); } /** * Serialize the TungstenProperties into a JSON String * * @param prettyPrint Set to true to have the JSON output formatted for * easier read * @return String representing JSON serialization of the TungstenProperties * @throws JsonGenerationException * @throws JsonMappingException * @throws IOException */ public String toJSON(boolean prettyPrint) throws JsonGenerationException, JsonMappingException, IOException { String json = null; ObjectMapper mapper = new ObjectMapper(); // Setup Jackson mapper.configure(Feature.INDENT_OUTPUT, true); mapper.configure(Feature.SORT_PROPERTIES_ALPHABETICALLY, true); ObjectWriter writer = mapper.writer(); if (prettyPrint) writer = writer.withDefaultPrettyPrinter(); json = writer.writeValueAsString(this); return json; } public static String formatProperties(String name, TungstenProperties props, String header) { String indent = " "; StringBuilder builder = new StringBuilder(); builder.append(header); builder.append(String.format("name = %s\n", name)).append(header); builder.append("{\n"); Map<String, String> propMap = props.hashMap(); for (String key : propMap.keySet()) { builder.append(String.format("%s%s = %s\n", indent, key, propMap.get(key))); } builder.append(String.format("}")); return builder.toString(); } /** * Receives properties from given stream.<br> * This function uses a in-house protocol consisting in having, for each * key/value pair, 1 line for key, 1 line for the class name and 1 for * value. The end of the transmission is identified by a predefined key name * {@value #ENDOFPROPERTIES_TAG} * * @see #send(PrintWriter) * @param in a ready-to-be-read buffered reader from which to get properties * @return a new set of properties containing data read on the stream * @throws IOException upon error while reading on the given input stream, * or if no data can be read at all * @throws ClassNotFoundException * @throws NoSuchMethodException * @throws SecurityException * @throws InvocationTargetException * @throws IllegalAccessException * @throws InstantiationException * @throws IllegalArgumentException */ public static TungstenProperties createFromStream(BufferedReader in) throws IOException { String key = in.readLine(); String valueClass = null; String valueLine = null; TungstenProperties tp = new TungstenProperties(); while (key != null && !key.equals(ENDOFPROPERTIES_TAG)) { valueClass = in.readLine(); if (valueClass == null) { throw new IOException( "Cannot create properties from stream: " + "reached end of stream before end of properties tag\n" + "Properties received till now:\n" + tp + "\n" + "last property key=" + key + " value class=" + valueClass + " value=" + valueLine); } else if ("null".equals(valueClass)) { tp.put(key, null); } else { Object value = null; if (TungstenProperties.class.getName().equals(valueClass)) { value = createFromStream(in); } else { valueLine = in.readLine(); if (valueLine == null) throw new IOException( "Cannot create properties from stream: " + "reached end of stream before end of properties tag"); while (!valueLine.endsWith(ENDOFLINE_TAG)) { String nextLine = in.readLine(); if (nextLine == null) throw new IOException( "Cannot create properties from stream: " + "reached end of stream before end of properties tag"); valueLine = valueLine + "\n" + nextLine; } valueLine = valueLine.substring(0, valueLine.length() - ENDOFLINE_TAG.length()); try { Constructor<?> ctor = Class.forName(valueClass) .getConstructor(String.class); value = ctor.newInstance(valueLine); } catch (Exception e) { String message = "Could not instanciate property class " + valueClass + " with value " + value; if (logger.isDebugEnabled()) { logger.debug(message, e); } IOException toThrow = new IOException(message); toThrow.setStackTrace(e.getStackTrace()); throw toThrow; } } tp.put(key, value); } key = in.readLine(); } // Data consistency check: the last key received must be the end of // properties tag, otherwise an error has occurred and we must throw an // exception if (!ENDOFPROPERTIES_TAG.equals(key)) throw new IOException("Cannot create properties from stream: " + "reached end of stream before end of properties tag"); return tp; } /** * Sends this object's set of properties on the given stream. <br> * This function uses a in-house protocol consisting in having, for each * key/value pair, 1 line for key an 1 for value. The end of the * transmission is identified by a predefined key name * {@value #ENDOFPROPERTIES_TAG} * * @see #createFromStream(BufferedReader) * @param out a prepared PrintWriter output stream on which to send * properties */ public void send(PrintWriter out) { for (String key : keyNames()) { out.println(key); Object value = getObject(key); if (value == null) out.println("null"); else { out.println(value.getClass().getName()); if (value instanceof TungstenProperties) { ((TungstenProperties) value).send(out); } else { out.println(value.toString() + ENDOFLINE_TAG); } } } out.println(ENDOFPROPERTIES_TAG); out.flush(); } /** * Returns true if the class in question is supports JavaBean conventions by * having a default constructor and setters/getters for properties. */ public boolean isBean(Class<?> clazz) { // Return false if we don't support beans. if (!this.isBeanSupportEnabled()) return false; // Check for a default constructor. boolean hasDefaultConstructor = false; for (Constructor<?> constructor : clazz.getConstructors()) { if (constructor.getParameterTypes().length == 0) { hasDefaultConstructor = true; break; } } if (!hasDefaultConstructor) return false; // Check that we have at least one setter. Map<String, Method> setters = getMethods(clazz, "set"); if (setters.size() == 0) return false; // Check that each setter has a matching getter. Map<String, Method> getters = getMethods(clazz, "get"); Map<String, Method> booleanGetters = getMethods(clazz, "is"); for (String name : setters.keySet()) { String getterName = "get" + name.substring(3); String booleanGetterName = "is" + name.substring(3); if (getters.containsKey(getterName) || booleanGetters.containsKey(booleanGetterName)) continue; else return false; } // Looks like a bean. return true; } // Returns all methods that start with a particular prefix and have // at least one additional character beyond to represent the property // name. private Map<String, Method> getMethods(Class<?> clazz, String prefix) { Map<String, Method> methodMap = new HashMap<String, Method>(); for (Method method : clazz.getMethods()) { String name = method.getName(); if (name.startsWith(prefix) && name.length() > prefix.length()) methodMap.put(method.getName(), method); } return methodMap; } /** * Generate a comma-delimited string of list items. */ static public String listToString(List<String> list) { StringBuilder builder = new StringBuilder(); int itemCount = 0; for (String value : list) { itemCount++; if (itemCount > 1) { builder.append(","); } builder.append(value); } return builder.toString(); } /** * Loads values from Java properties file format with variable * substitutions. */ public void add(InputStream is) throws IOException { add(is, true); } /** * Adds values from Java properties file format. Current values are kept * except if it exists in the stream, in which case it is overwritten. * * @param is InputStream containing properties. * @param doSubstitutions If true perform variable substitutions */ public void add(InputStream is, boolean doSubstitutions) throws IOException { // Load the properties file. Properties props = new Properties(); props.load(is); if (doSubstitutions) substituteSystemValues(props); add(props); } /** * Load values from a Properties instance. Current values are replaced only * if they are in the source map. */ public void add(Properties props) { Enumeration<?> keys = props.propertyNames(); while (keys.hasMoreElements()) { String key = (String) keys.nextElement(); String value = props.getProperty(key).toString(); if (properties.get(key) != null) { if (logger.isDebugEnabled()) { logger.debug(String.format("Replacing %s=%s with %s=%s", key, properties.get(key), key, value)); } } properties.put(key, value); } } }