/*
* Copyright (c) 2014 Oculus Info Inc. http://www.oculusinfo.com/
*
* Released under the MIT License.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.oculusinfo.factory;
import java.io.PrintStream;
import java.security.MessageDigest;
import java.util.*;
import org.apache.commons.codec.binary.Hex;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class provides a basis for factories that are configurable via JSON or
* java property files.
*
* This provides the standard glue of getting properties consistently in either
* case, of documenting the properties needed by a factory, and, potentially, of
* writing out a configuration.
*
* @param <T> The type of object constructed by this factory.
*
* @author nkronenfeld
*/
abstract public class ConfigurableFactory<T> {
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigurableFactory.class);
public static List<String> mergePaths (ConfigurableFactory<?> parent, List<String> childPath) {
List<String> parentPath = null;
if (null != parent) parentPath = parent.getRootPath();
return mergePaths(parentPath, childPath);
}
public static List<String> mergePaths (List<String> parentPath, List<String> childPath) {
List<String> fullPath = new ArrayList<>();
if (null != parentPath) fullPath.addAll(parentPath);
if (null != childPath) fullPath.addAll(childPath);
return fullPath;
}
private String _name;
private Class<T> _factoryType;
private List<String> _rootPath;
private ConfigurableFactory<?> _parent;
private List<ConfigurableFactory<?>> _children;
private Set<ConfigurationProperty<?>> _properties;
private boolean _configured;
private JSONObject _configurationNode;
private Map<ConfigurationProperty<?>, Object> _defaultValues;
private boolean _isSingleton;
private T _singletonProduct;
private Map<ConfigurationProperty<?>, List<String>> _pathsByProperty;
/**
* Create a factory
*
* @param factoryType The type of object to be constructed by this factory.
* Can not be null.
* @param parent The parent factory; all configuration nodes of this factory
* will be under the parent factory's root configuration node.
* @param path The path from the parent factory's root configuration node to
* this factory's root configuration node.
*/
protected ConfigurableFactory (Class<T> factoryType, ConfigurableFactory<?> parent, List<String> path) {
this(null, factoryType, parent, path, false);
}
/**
* Create a factory
*
* @param factoryType The type of object to be constructed by this factory.
* Can not be null.
* @param parent The parent factory; all configuration nodes of this factory
* will be under the parent factory's root configuration node.
* @param path The path from the parent factory's root configuration node to
* this factory's root configuration node.
* @param isSingleton If true, this factory will only ever produce one
* product, which it will return every time it is asked to
* produce. Do note that if the factory is set to produce a
* singleton, production may be a synchronized, blocking
* operation.
*/
protected ConfigurableFactory (Class<T> factoryType, ConfigurableFactory<?> parent, List<String> path, boolean isSingleton) {
this(null, factoryType, parent, path, isSingleton);
}
/**
* Create a factory
*
* @param name A name by which this factory can be known, to be used to
* differentiate it from other child factories of this factory's
* parent that return the same type.
* @param factoryType The type of object to be constructed by this factory.
* Can not be null.
* @param parent The parent factory; all configuration nodes of this factory
* will be under the parent factory's root configuration node.
* @param path The path from the parent factory's root configuration node to
* this factory's root configuration node.
*/
protected ConfigurableFactory (String name, Class<T> factoryType, ConfigurableFactory<?> parent, List<String> path) {
this(name, factoryType, parent, path, false);
}
/**
* Create a factory
*
* @param name A name by which this factory can be known, to be used to
* differentiate it from other child factories of this factory's
* parent that return the same type.
* @param factoryType The type of object to be constructed by this factory.
* Can not be null.
* @param parent The parent factory; all configuration nodes of this factory
* will be under the parent factory's root configuration node.
* @param path The path from the parent factory's root configuration node to
* this factory's root configuration node.
* @param isSingleton If true, this factory will only ever produce one
* product, which it will return every time it is asked to
* produce. Do note that if the factory is set to produce a
* singleton, production may be a synchronized, blocking
* operation.
*/
protected ConfigurableFactory (String name, Class<T> factoryType, ConfigurableFactory<?> parent, List<String> path, boolean isSingleton) {
_name = name;
_factoryType = factoryType;
_rootPath = Collections.unmodifiableList(mergePaths(parent, path));
_children = new ArrayList<>();
_configured = false;
_properties = new HashSet<>();
_pathsByProperty = new HashMap<>();
_defaultValues = new HashMap<>();
_isSingleton = isSingleton;
_singletonProduct = null;
//NOTE: this should not be set to the parent passed in cause the parent won't necessarily
//be created before the children if you're doing a bottom up approach for some reason.
_parent = null;
}
/**
* Get the root node in the tree of configurables.
* @return Returns the root of the configurable factories, or this factory if no parent is set.
*/
public ConfigurableFactory<?> getRoot() {
return (_parent != null)? _parent.getRoot() : this;
}
/**
* Get the root path for configuration information for this factory.
*
* @return A list of strings describing the path to this factory's
* configuration information. Guaranteed not to be null.
*/
public List<String> getRootPath () {
return new ArrayList<>( _rootPath );
}
/**
* Get the name associated with the factory.
* @return A string name if provided upon construction, or else null if none was provided.
*/
public String getName() {
return _name;
}
/**
* List out all properties directly expected by this factory.
*/
public Iterable<ConfigurationProperty<?>> getProperties () {
return _properties;
}
/**
* Add a property to the list of properties used by this factory
*
* @param property
* @param path
*/
public <PT> void addProperty (ConfigurationProperty<PT> property, List<String> path) {
_properties.add(property);
_pathsByProperty.put( property, path );
}
/** Get the full name of a given property, including its path in our factory configuration */
public String getFullPropertyName (ConfigurationProperty<?> property) {
if (null == property) return null;
List<String> fullPath = new ArrayList<>();
if (null != _rootPath) fullPath.addAll(_rootPath);
if (null != _pathsByProperty.get(property)) fullPath.addAll(_pathsByProperty.get(property));
fullPath.add(property.getName());
return mkString(fullPath, ".");
}
/**
* Add a property to the list of properties used by this factory
*
* @param property
*/
public <PT> void addProperty (ConfigurationProperty<PT> property) {
addProperty( property, new ArrayList<String>() );
}
/**
* Set the default value of a property for this factory, and this factory only.
*/
public <PT> void setDefaultValue (ConfigurationProperty<PT> property, PT defaultValue) {
_defaultValues.put(property, defaultValue);
}
/**
* gets the default value for a given property for this factory.
*/
protected <PT> PT getDefaultValue (ConfigurationProperty<PT> property) {
if (_defaultValues.containsKey(property)) {
return property.getType().cast(_defaultValues.get(property));
} else {
return property.getDefaultValue();
}
}
/**
* Return a SHA-256 hexcode representing the state of the configuration
* @return String representing the hexcode SHA-256 hash of the configuration state
*/
public String generateSHA256() {
try {
String propertyString = getFactoryString();
// generate SHA-256 from the string
MessageDigest md = MessageDigest.getInstance( "SHA-256" );
md.update( propertyString.getBytes( "UTF-8" ) );
byte[] digest = md.digest();
// convert SHA-256 bytes to hex string
return Hex.encodeHexString( digest );
} catch ( Exception e ) {
LOGGER.warn( "Error registering configuration to SHA", e );
return "";
}
}
/**
* Indicates if an actual value is recorded for the given property.
*
* @param property The property of interest.
* @return True if the property is listed and non-default in the factory.
*/
public boolean hasPropertyValue (ConfigurationProperty<?> property) {
return (_configured && null != _configurationNode && getPropertyNode( property ).has( property.getName() ) );
}
/**
* Get the value read at configuration time for the given property.
*
* The behavior of this function is undefined if called before
* readConfiguration (either version).
*/
public <PT> PT getPropertyValue (ConfigurationProperty<PT> property) throws ConfigurationException {
// if a value has not been configured for this property, return default
if ( !hasPropertyValue( property ) ) {
PT defaultValue = getDefaultValue(property);
LOGGER.warn("Property {} unset. Using default {}.", getFullPropertyName(property), defaultValue);
return defaultValue;
}
try {
return property.unencodeJSON( new JSONNode( getPropertyNode( property ) , property.getName() ) );
} catch (JSONException e) {
// Must not have been there. Ignore, leaving as default.
throw new ConfigurationException("Error reading property "+getFullPropertyName(property)+" from configuration "+_configurationNode);
}
}
/**
* Add a child factory, to be used by this factory.
*
* @param child The child to add.
*/
public void addChildFactory (ConfigurableFactory<?> child) {
_children.add(child);
child._parent = this;
}
/**
* Create the object provided by this factory.
* @return The object
*/
protected abstract T create() throws ConfigurationException;
/**
* Get one of the goods managed by this factory.
*
* This version returns a new instance each time it is called.
*
* @param goodsType The type of goods desired.
*/
public <GT> GT produce (Class<GT> goodsType) throws ConfigurationException {
return produce(null, goodsType);
}
/**
* Get one of the goods managed by this factory.
*
* This version returns a new instance each time it is called.
*
* @param name The name of the factory from which to obtain the needed
* goods. Null indicates that the factory name doesn't matter.
* @param goodsType The type of goods desired.
*/
public <GT> GT produce (String name, Class<GT> goodsType) throws ConfigurationException {
if (!_configured) {
throw new ConfigurationException("Attempt to get value from uninitialized factory");
}
if ((null == name || name.equals(_name)) && goodsType.equals(_factoryType)) {
if (_isSingleton) {
if (null == _singletonProduct) {
synchronized (this) {
if (null == _singletonProduct) {
_singletonProduct = create();
}
}
}
return goodsType.cast(_singletonProduct);
} else {
return goodsType.cast(create());
}
} else {
for (ConfigurableFactory<?> child: _children) {
GT result = child.produce(name, goodsType);
if (null != result) return result;
}
}
return null;
}
public <GT> ConfigurableFactory<GT> getProducer (Class<GT> goodsType) {
return getProducer(null, goodsType);
}
// We are suppressing the warnings in the
// return this;
// line. We have just, in the line before, checked that goodsType - which is
// Class<GT> - matches _factoryType - which is Class<T> - so therefore, GT
// and T must be the same, so this cast is guaranteed safe, even if the
// compiler can't figure that out.
@SuppressWarnings({"unchecked", "rawtypes"})
public <GT> ConfigurableFactory<GT> getProducer (String name, Class<GT> goodsType) {
if ((null == name || name.equals(_name)) && goodsType.equals(_factoryType)) {
return (ConfigurableFactory) this;
} else {
for (ConfigurableFactory<?> child: _children) {
ConfigurableFactory<GT> result = child.getProducer(name, goodsType);
if (null != result) return result;
}
}
return null;
}
/**
* Initialize needed construction values from a properties list.
*
* @param rootNode The root node of all configuration information for this
* factory.
* @throws ConfigurationException If something goes wrong in configuration.
*/
public void readConfiguration (JSONObject rootNode) throws ConfigurationException {
try {
_configurationNode = getConfigurationNode(rootNode);
for (ConfigurableFactory<?> child: _children) {
child.readConfiguration(rootNode);
}
_configured = true;
} catch (JSONException e) {
throw new ConfigurationException("Error configuring factory "+this.getClass().getName(), e);
}
}
public void writeConfigurationInformation (PrintStream stream, String prefix) {
stream.println(prefix+"Configuration for "+this.getClass().getSimpleName()+" (node name "+_name+", path: "+mkString(_rootPath, ", ")+"):");
prefix = prefix + " ";
for (ConfigurationProperty<?> property: _properties) {
writePropertyValue(stream, prefix, property);
}
stream.println();
for (ConfigurableFactory<?> child: _children) {
child.writeConfigurationInformation(stream, prefix);
}
}
public void writeConfigurationInformation (PrintStream stream) {
writeConfigurationInformation(stream, "");
}
/**
* Get the explicit configuration JSON, containing ALL properties.
* @return JSONObject containing all properties used by the configuration
*/
public JSONObject getExplicitConfiguration() {
JSONObject config = new JSONObject();
return generateConfigurationObj( config );
}
/**
* Get the JSON object used to configure this factory.
*
* @return The configuring JSON object, or null if this factory has not yet
* been configured.
*/
protected JSONObject getConfigurationNode () {
return _configurationNode;
}
/**
* Gets the class of object produced by this factory.
*/
protected Class<? extends T> getFactoryType () {
return _factoryType;
}
private JSONObject getPropertyNode( ConfigurationProperty<?> property ) {
if ( _pathsByProperty.get( property ) == null ) {
return new JSONObject();
}
JSONObject node;
List<String> path = new ArrayList<>( _pathsByProperty.get( property ) );
if ( path.isEmpty() ) {
return _configurationNode;
} else {
String subPath;
JSONObject currentNode = _configurationNode;
while ( path.size() > 1 ) {
subPath = path.remove( 0 );
currentNode = currentNode.optJSONObject( subPath );
if ( currentNode == null ) {
return new JSONObject();
}
}
node = currentNode.optJSONObject( path.get(0) );
if ( node == null ) {
return new JSONObject();
}
return node;
}
}
/*
* Gets the JSON node with all this factory's configuration information
*
* @param rootNode The root JSON node containing all configuration
* information.
*/
private JSONObject getConfigurationNode (JSONObject rootNode) throws JSONException {
return getLeafNode(rootNode, _rootPath);
}
/**
* Get the sub-node of a root node specified by a given path.
*
* @param rootNode The root JSON object whose sub-node is desired.
* @param path A list of keys to follow from the root node to find the
* desired leaf.
* @return The leaf node, or null if any branch along the path is missing.
*/
public static JSONObject getLeafNode (JSONObject rootNode, List<String> path) {
JSONObject target = rootNode;
for (String subpath: path) {
if (target.has( subpath )) {
try {
target = target.getJSONObject( subpath );
} catch (JSONException e) {
// Node is of the wrong type; default everything.
target = null;
break;
}
} else {
target = null;
break;
}
}
return target;
}
private <PT> void writePropertyValue (PrintStream stream, String prefix, ConfigurationProperty<PT> property) {
if (hasPropertyValue(property)) {
stream.println(prefix+property.getName()+": "+property.encode(getDefaultValue(property))+" (DEFAULT)");
} else {
PT value;
try {
value = property.unencodeJSON(new JSONNode(_configurationNode, property.getName()));
stream.println(prefix+property.getName()+": "+property.encode(value));
} catch (JSONException|ConfigurationException e) {
stream.println(prefix+property.getName()+": "+property.encode(getDefaultValue(property))+" (DEFAULT - read error)");
}
}
}
private String mkString (List<?> list, String separator) {
if (null == list) return "null";
boolean first = true;
String result = "";
for (Object elt: list) {
if (first) first = false;
else result += separator;
result = result + elt;
}
return result;
}
private String getFullPropertyString( ConfigurationProperty<?> property, String name ) {
StringBuilder sb = new StringBuilder();
for ( String subPath : _rootPath ) {
sb.append( subPath );
sb.append(".");
}
if ( _pathsByProperty.get( property ) != null ) {
List<String> attributePath = new ArrayList<>( _pathsByProperty.get( property ) );
for ( String subPath : attributePath ) {
sb.append( subPath );
sb.append( "." );
}
}
sb.append( name );
return sb.toString();
}
private String getFactoryString() {
StringBuilder sb = new StringBuilder();
for ( ConfigurationProperty<?> prop : _properties ) {
sb.append( getFullPropertyString( prop, prop.getName() ) );
sb.append( ":" );
try {
sb.append(getPropertyValue(prop));
} catch (ConfigurationException e) {
sb.append("<ERROR>");
}
}
for ( ConfigurableFactory<?> child: _children ) {
sb.append( child.getFactoryString() );
}
return sb.toString();
}
private JSONObject addJSONPathAndReturnLeaf( JSONObject config, List<String> path ) throws JSONException {
// if a path does not exist in the json object, create it
JSONObject node = config;
for ( String subpath : path ) {
if ( !node.has( subpath ) ) {
node.put( subpath, new JSONObject() );
}
node = node.getJSONObject( subpath );
}
// return the leaf node of the path
return node;
}
private void addPropertyUnderPath( JSONObject config, ConfigurationProperty<?> property ) throws JSONException, ConfigurationException {
// get the properties path
List<String> fullPath = getRootPath();
fullPath.addAll(_pathsByProperty.get( property ) );
// ensure the path exists by adding it if it doesn't
// append root path to start of property path since they are relative to the
// current factories path
JSONObject node = addJSONPathAndReturnLeaf( config, fullPath );
// add the property to the leaf node of the path
node.put(property.getName(), getPropertyValue(property));
}
private JSONObject generateConfigurationObj( JSONObject config ) {
try {
for ( ConfigurationProperty<?> prop : _properties ) {
// add property to the config object under its path
addPropertyUnderPath( config, prop );
}
for ( ConfigurableFactory<?> child: _children ) {
addJSONPathAndReturnLeaf( config, child.getRootPath() );
child.generateConfigurationObj( config );
}
} catch ( JSONException | ConfigurationException e ) {
LOGGER.warn( "Error occurred while generating configuration JSON", e );
}
return config;
}
}