com.oculusinfo.factory.ConfigurableFactory.java Source code

Java tutorial

Introduction

Here is the source code for com.oculusinfo.factory.ConfigurableFactory.java

Source

/*
 * 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 boolean _isSingleton;
    private T _singletonProduct;
    private HashMap<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<>();
        _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);
    }

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

    /**
     * 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) {
        // if a value has not been configured for this property, return default
        if (!hasPropertyValue(property)) {
            return property.getDefaultValue();
        }
        try {
            return property.unencodeJSON(new JSONNode(getPropertyNode(property), property.getName()));
        } catch (JSONException e) {
            // Must not have been there.  Ignore, leaving as default. 
            LOGGER.info("Property {} from configuration {} not found. Using default", property, _configurationNode);
        } catch (ConfigurationException e) {
            // Error within configuration.
            // Use default, but also warn about it.
            LOGGER.warn("Error reading property {} from configuration {}", property, _configurationNode);
        }
        return property.getDefaultValue();
    }

    /**
     * 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();

    /**
     * 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(property.getDefaultValue())
                    + " (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(property.getDefaultValue())
                        + " (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(":");
            sb.append(getPropertyValue(prop));
        }
        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 {
        // 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 e) {
            LOGGER.warn("Error occurred while generating configuration JSON", e);
        }
        return config;
    }

}