Props.java

973 lines | 24.45 kB Blame History Raw Download
/*
 * Copyright 2012 LinkedIn, Inc
 * 
 * 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.
 */

package azkaban.utils;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.log4j.Logger;

/**
 * Hashmap implementation of a hierarchitical properties with helpful converter
 * functions and Exception throwing. This class is not threadsafe.
 */
public class Props {
    private final Map<String, String> _current;
    private Props _parent;
    private String source = null;

    /**
     * Constructor for empty props with empty parent.
     */
    public Props() {
        this(null);
    }

    /**
     * Constructor for empty Props with parent override.
     * 
     * @param parent
     */
    public Props(Props parent) {
        this._current = new HashMap<String, String>();
        this._parent = parent;
    }

    /**
     * Load props from a file.
     * 
     * @param parent
     * @param file
     * @throws IOException
     */
    public Props(Props parent, String filepath) throws IOException {
        this(parent, new File(filepath));
    }
    
    /**
     * Load props from a file.
     * 
     * @param parent
     * @param file
     * @throws IOException
     */
    public Props(Props parent, File file) throws IOException {
        this(parent);
        setSource(file.getPath());

        InputStream input = new BufferedInputStream(new FileInputStream(file));
        try {
            loadFrom(input);
        }
        catch (IOException e) {
            input.close();
            throw e;
        }
        input.close();
    }

    /**
     * Create props from property input streams
     * 
     * @param parent
     * @param inputStreams
     * @throws IOException
     */
    public Props(Props parent, InputStream inputStream) throws IOException {
        this(parent);
        loadFrom(inputStream);
    }

    /**
     * 
     * @param inputStream
     * @throws IOException
     */
    private void loadFrom(InputStream inputStream) throws IOException {
        Properties properties = new Properties();
        properties.load(inputStream);
        this.put(properties);
    }

    /**
     * Create properties from maps of properties
     * 
     * @param parent
     * @param props
     */
    public Props(Props parent, Map<String, String>... props) {
        this(parent);
        for (int i = props.length - 1; i >= 0; i--) {
            this.putAll(props[i]);
        }
    }

    /**
     * Create properties from Properties objects
     * 
     * @param parent
     * @param properties
     */
    public Props(Props parent, Properties... properties) {
        this(parent);
        for (int i = properties.length - 1; i >= 0; i--) {
            this.put(properties[i]);
        }
    }

    /**
     * Create a Props object with the contents set to that of props.
     * 
     * @param parent
     * @param props
     */
    public Props(Props parent, Props props) {
        this(parent);
        if (props != null) {
            putAll(props);
        }
    }

    /**
     * Create a Props with a null parent from a list of key value pairing. i.e.
     * [key1, value1, key2, value2 ...]
     * 
     * @param args
     * @return
     */
    public static Props of(String... args) {
        return of((Props) null, args);
    }

    /**
     * Create a Props from a list of key value pairing. i.e. [key1, value1,
     * key2, value2 ...]
     * 
     * @param args
     * @return
     */
    @SuppressWarnings("unchecked")
    public static Props of(Props parent, String... args) {
        if (args.length % 2 != 0) {
            throw new IllegalArgumentException("Must have an equal number of keys and values.");
        }

        Map<String, String> vals = new HashMap<String, String>(args.length / 2);

        for (int i = 0; i < args.length; i += 2) {
            vals.put(args[i], args[i + 1]);
        }
        return new Props(parent, vals);
    }

    /**
     * Clear the current Props, but leaves the parent untouched.
     */
    public void clearLocal() {
        _current.clear();
    }

    /**
     * Check key in current Props then search in parent
     * 
     * @param k
     * @return
     */
    public boolean containsKey(Object k) {
        return _current.containsKey(k) || (_parent != null && _parent.containsKey(k));
    }

    /**
     * Check value in current Props then search in parent
     * 
     * @param value
     * @return
     */
    public boolean containsValue(Object value) {
        return _current.containsValue(value) || (_parent != null && _parent.containsValue(value));
    }

    /**
     * Return value if available in current Props otherwise return from parent
     * 
     * @param key
     * @return
     */
    public String get(Object key) {
        if (_current.containsKey(key)) {
            return _current.get(key);
        }
        else if (_parent != null) {
            return _parent.get(key);
        }
        else {
            return null;
        }
    }

    /**
     * Get the key set from the current Props
     * 
     * @return
     */
    public Set<String> localKeySet() {
        return _current.keySet();
    }

    /**
     * Get parent Props
     * 
     * @return
     */
    public Props getParent() {
        return _parent;
    }

    /**
     * Put the given string value for the string key. This method performs any
     * variable substitution in the value replacing any occurance of ${name}
     * with the value of get("name").
     * 
     * @param key
     *            The key to put the value to
     * @param value
     *            The value to do substitution on and store
     * 
     * @throws IllegalArgumentException
     *             If the variable given for substitution is not a valid key in
     *             this Props.
     */
    public String put(String key, String value) {
        return _current.put(key, value);
    }

    /**
     * Put the given Properties into the Props. This method performs any
     * variable substitution in the value replacing any occurrence of ${name}
     * with the value of get("name"). get() is called first on the Props and
     * next on the Properties object.
     * 
     * @param properties
     *            The properties to put
     * 
     * @throws IllegalArgumentException
     *             If the variable given for substitution is not a valid key in
     *             this Props.
     */
    public void put(Properties properties) {
        for (String propName : properties.stringPropertyNames()) {
            _current.put(propName, properties.getProperty(propName));
        }
    }

    /**
     * Put integer
     * 
     * @param key
     * @param value
     * @return
     */
    public String put(String key, Integer value) {
        return _current.put(key, value.toString());
    }

    /**
     * Put Long. Stores as String.
     * 
     * @param key
     * @param value
     * @return
     */
    public String put(String key, Long value) {
        return _current.put(key, value.toString());
    }

    /**
     * Put Double. Stores as String.
     * 
     * @param key
     * @param value
     * @return
     */
    public String put(String key, Double value) {
        return _current.put(key, value.toString());
    }

    /**
     * Put everything in the map into the props.
     * 
     * @param m
     */
    public void putAll(Map<? extends String, ? extends String> m) {
        if (m == null) {
            return;
        }

        for (Map.Entry<? extends String, ? extends String> entry : m.entrySet())
        {
            this.put(entry.getKey(), entry.getValue());
        }
    }

    /**
     * Put all properties in the props into the current props. Will handle null
     * p.
     * 
     * @param p
     */
    public void putAll(Props p) {
        if (p == null) {
            return;
        }

        for (String key : p.getKeySet()) {
            this.put(key, p.get(key));
        }
    }

    /**
     * Puts only the local props from p into the current properties
     * 
     * @param p
     */
    public void putLocal(Props p) {
        for (String key : p.localKeySet()) {
            this.put(key, p.get(key));
        }
    }

    /**
     * Remove only the local value of key s, and not the parents.
     * 
     * @param s
     * @return
     */
    public String removeLocal(Object s) {
        return _current.remove(s);
    }

    /**
     * The number of unique keys defined by this Props and all parent Props
     */
    public int size() {
        return getKeySet().size();
    }

    /**
     * The number of unique keys defined by this Props (keys defined only in
     * parent Props are not counted)
     */
    public int localSize() {
        return _current.size();
    }

    /**
     * Attempts to return the Class that corresponds to the Props value. If the
     * class doesn't exit, an IllegalArgumentException will be thrown.
     * 
     * @param key
     * @return
     */
    public Class<?> getClass(String key) {
        try {
            if (containsKey(key)) {
                return Class.forName(get(key));
            }
            else {
                throw new UndefinedPropertyException("Missing required property '" + key + "'");
            }
        }
        catch (ClassNotFoundException e) {
            throw new IllegalArgumentException(e);
        }
    }

    /**
     * Gets the class from the Props. If it doesn't exist, it will return the
     * defaultClass
     * 
     * @param key
     * @param c
     * @return
     */
    public Class<?> getClass(String key, Class<?> defaultClass) {
        if (containsKey(key)) {
            return getClass(key);
        }
        else {
            return defaultClass;
        }
    }

    /**
     * Gets the string from the Props. If it doesn't exist, it will return the
     * defaultValue
     * 
     * @param key
     * @param defaultValue
     * @return
     */
    public String getString(String key, String defaultValue) {
        if (containsKey(key)) {
            return get(key);
        }
        else {
            return defaultValue;
        }
    }

    /**
     * Gets the string from the Props. If it doesn't exist, throw and
     * UndefinedPropertiesException
     * 
     * @param key
     * @param defaultValue
     * @return
     */
    public String getString(String key) {
        if (containsKey(key)) {
            return get(key);
        }
        else {
            throw new UndefinedPropertyException("Missing required property '" + key + "'");
        }
    }

    /**
     * Returns a list of strings with the comma as the separator of the value
     * 
     * @param key
     * @return
     */
    public List<String> getStringList(String key) {
        return getStringList(key, "\\s*,\\s*");
    }

    /**
     * Returns a list of strings with the sep as the separator of the value
     * 
     * @param key
     * @param sep
     * @return
     */
    public List<String> getStringList(String key, String sep) {
        String val = get(key);
        if (val == null || val.trim().length() == 0) {
            return Collections.emptyList();
        }

        if (containsKey(key)) {
            return Arrays.asList(val.split(sep));
        }
        else {
            throw new UndefinedPropertyException("Missing required property '" + key + "'");
        }
    }

    /**
     * Returns a list of strings with the comma as the separator of the value.
     * If the value is null, it'll return the defaultValue.
     * 
     * @param key
     * @return
     */
    public List<String> getStringList(String key, List<String> defaultValue) {
        if (containsKey(key)) {
            return getStringList(key);
        }
        else {
            return defaultValue;
        }
    }

    /**
     * Returns a list of strings with the sep as the separator of the value. If
     * the value is null, it'll return the defaultValue.
     * 
     * @param key
     * @return
     */
    public List<String> getStringList(String key, List<String> defaultValue, String sep) {
        if (containsKey(key)) {
            return getStringList(key, sep);
        }
        else {
            return defaultValue;
        }
    }

    /**
     * Returns true if the value equals "true". If the value is null, then the
     * default value is returned.
     * 
     * @param key
     * @param defaultValue
     * @return
     */
    public boolean getBoolean(String key, boolean defaultValue) {
        if (containsKey(key)) {
            return "true".equalsIgnoreCase(get(key).trim());
        }
        else {
            return defaultValue;
        }
    }

    /**
     * Returns true if the value equals "true". If the value is null, then an
     * UndefinedPropertyException is thrown.
     * 
     * @param key
     * @return
     */
    public boolean getBoolean(String key) {
        if (containsKey(key)) return "true".equalsIgnoreCase(get(key));
        else throw new UndefinedPropertyException("Missing required property '" + key + "'");
    }

    /**
     * Returns the long representation of the value. If the value is null, then
     * the default value is returned. If the value isn't a long, then a parse
     * exception will be thrown.
     * 
     * @param key
     * @param defaultValue
     * @return
     */
    public long getLong(String name, long defaultValue) {
        if (containsKey(name)) {
            return Long.parseLong(get(name));
        }
        else {
            return defaultValue;
        }
    }

    /**
     * Returns the long representation of the value. If the value is null, then
     * a UndefinedPropertyException will be thrown. If the value isn't a long,
     * then a parse exception will be thrown.
     * 
     * @param key
     * @return
     */
    public long getLong(String name) {
        if (containsKey(name)) {
            return Long.parseLong(get(name));
        }
        else {
            throw new UndefinedPropertyException("Missing required property '" + name + "'");
        }
    }

    /**
     * Returns the int representation of the value. If the value is null, then
     * the default value is returned. If the value isn't a int, then a parse
     * exception will be thrown.
     * 
     * @param key
     * @param defaultValue
     * @return
     */
    public int getInt(String name, int defaultValue) {
        if (containsKey(name)) {
            return Integer.parseInt(get(name).trim());
        }
        else {
            return defaultValue;
        }
    }

    /**
     * Returns the int representation of the value. If the value is null, then a
     * UndefinedPropertyException will be thrown. If the value isn't a int, then
     * a parse exception will be thrown.
     * 
     * @param key
     * @return
     */
    public int getInt(String name) {
        if (containsKey(name)) {
            return Integer.parseInt(get(name).trim());
        }
        else {
            throw new UndefinedPropertyException("Missing required property '" + name + "'");
        }
    }

    /**
     * Returns the double representation of the value. If the value is null,
     * then the default value is returned. If the value isn't a double, then a
     * parse exception will be thrown.
     * 
     * @param key
     * @param defaultValue
     * @return
     */
    public double getDouble(String name, double defaultValue) {
        if (containsKey(name)) {
            return Double.parseDouble(get(name).trim());
        }
        else {
            return defaultValue;
        }
    }

    /**
     * Returns the double representation of the value. If the value is null,
     * then a UndefinedPropertyException will be thrown. If the value isn't a
     * double, then a parse exception will be thrown.
     * 
     * @param key
     * @return
     */
    public double getDouble(String name) {
        if (containsKey(name)) {
            return Double.parseDouble(get(name).trim());
        }
        else {
            throw new UndefinedPropertyException("Missing required property '" + name + "'");
        }
    }

    /**
     * Returns the uri representation of the value. If the value is null, then
     * the default value is returned. If the value isn't a uri, then a
     * IllegalArgumentException will be thrown.
     * 
     * @param key
     * @param defaultValue
     * @return
     */
    public URI getUri(String name) {
        if (containsKey(name)) {
            try {
                return new URI(get(name));
            }
            catch (URISyntaxException e) {
                throw new IllegalArgumentException(e.getMessage());
            }
        }
        else {
            throw new UndefinedPropertyException("Missing required property '" + name + "'");
        }
    }

    /**
     * Returns the double representation of the value. If the value is null,
     * then the default value is returned. If the value isn't a uri, then a
     * IllegalArgumentException will be thrown.
     * 
     * @param key
     * @param defaultValue
     * @return
     */
    public URI getUri(String name, URI defaultValue) {
        if (containsKey(name)) {
            return getUri(name);
        }
        else {
            return defaultValue;
        }
    }

    public URI getUri(String name, String defaultValue) {
        try {
            return getUri(name, new URI(defaultValue));
        }
        catch (URISyntaxException e) {
            throw new IllegalArgumentException(e.getMessage());
        }
    }

    /**
     * Store only those properties defined at this local level
     * 
     * @param file
     *            The file to write to
     * @throws IOException
     *             If the file can't be found or there is an io error
     */
    public void storeLocal(File file) throws IOException {
        BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file));
        try {
            storeLocal(out);
        }
        finally {
            out.close();
        }
    }

    /**
     * Returns a copy of only the local values of this props
     * 
     * @return
     */
    @SuppressWarnings("unchecked")
    public Props local() {
        return new Props(null, _current);
    }

    /**
     * Store only those properties defined at this local level
     * 
     * @param out
     *            The output stream to write to
     * @throws IOException
     *             If the file can't be found or there is an io error
     */
    public void storeLocal(OutputStream out) throws IOException {
        Properties p = new Properties();
        for (String key : _current.keySet()) {
            p.setProperty(key, get(key));
        }
        p.store(out, null);
    }

    /**
     * Returns a java.util.Properties file populated with the stuff in here.
     * 
     * @return
     */
    public Properties toProperties() {
        Properties p = new Properties();
        for (String key : _current.keySet()) {
            p.setProperty(key, get(key));
        }

        return p;
    }

    /**
     * Store all properties, those local and also those in parent props
     * 
     * @param file
     *            The file to store to
     * @throws IOException
     *             If there is an error writing
     */
    public void storeFlattened(File file) throws IOException {
        BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file));
        try {
            storeFlattened(out);
        }
        finally {
            out.close();
        }
    }

    /**
     * Store all properties, those local and also those in parent props
     * 
     * @param out
     *            The stream to write to
     * @throws IOException
     *             If there is an error writing
     */
    public void storeFlattened(OutputStream out) throws IOException {
        Properties p = new Properties();
        for (Props curr = this; curr != null; curr = curr.getParent()) {
            for (String key : curr.localKeySet()) {
                if (!p.containsKey(key)) {
                    p.setProperty(key, get(key));
                }
            }
        }

        p.store(out, null);
    }

    /**
     * Get a map of all properties by string prefix
     * 
     * @param prefix
     *            The string prefix
     */
    public Map<String, String> getMapByPrefix(String prefix) {
        Map<String, String> values = new HashMap<String, String>();

        if (_parent != null) {
            for (Map.Entry<String, String> entry : _parent.getMapByPrefix(prefix).entrySet())
            {
                values.put(entry.getKey(), entry.getValue());
            }
        }

        for (String key : this.localKeySet()) {
            if (key.startsWith(prefix)) {
                values.put(key.substring(prefix.length()), get(key));
            }
        }
        return values;
    }

    /**
     * Returns a set of all keys, including the parents
     * @return
     */
    public Set<String> getKeySet() {
        HashSet<String> keySet = new HashSet<String>();

        keySet.addAll(localKeySet());

        if (_parent != null) {
            keySet.addAll(_parent.getKeySet());
        }

        return keySet;
    }

    /**
     * Logs the property in the given logger
     * 
     * @param logger
     * @param comment
     */
    public void logProperties(Logger logger, String comment) {
        logger.info(comment);

        for (String key : getKeySet()) {
            logger.info("  key=" + key + " value=" + get(key));
        }
    }

    /**
     * Clones the Props p object and all of its parents.
     * 
     * @param p
     * @return
     */
    public static Props clone(Props p) {
        return copyNext(p);
    }

    /**
     * 
     * @param source
     * @return
     */
    private static Props copyNext(Props source) {
        Props priorNodeCopy = null;
        if (source.getParent() != null) {
            priorNodeCopy = copyNext(source.getParent());
        }
        Props dest = new Props(priorNodeCopy);
        for (String key : source.localKeySet()) {
            dest.put(key, source.get(key));
        }

        return dest;
    }

    /**
     */
    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }
        else if (o == null) {
            return false;
        }
        else if (o.getClass() != Props.class) {
            return false;
        }

        Props p = (Props) o;
        return _current.equals(p._current) && Utils.equals(this._parent, p._parent);
    }

    /**
     * Returns true if the properties are equivalent, regardless of the hierarchy.
     * 
     * @param p
     * @return
     */
    public boolean equalsProps(Props p) {
        if (p == null) {
            return false;
        }

        final Set<String> myKeySet = getKeySet();
        for (String s : myKeySet) {
            if (!get(s).equals(p.get(s))) {
                return false;
            }
        }

        return myKeySet.size() == p.getKeySet().size();
    }

    /**
     * 
     */
    @Override
    public int hashCode() {
        int code = this._current.hashCode();
        if (_parent != null) code += _parent.hashCode();
        return code;
    }

    /**
     * 
     */
    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder("{");
        for (Map.Entry<String, String> entry : this._current.entrySet()) {
            builder.append(entry.getKey());
            builder.append(": ");
            builder.append(entry.getValue());
            builder.append(", ");
        }
        if (_parent != null) {
            builder.append(" parent = ");
            builder.append(_parent.toString());
        }
        builder.append("}");
        return builder.toString();
    }

    public String getSource() {
        return source;
    }

    public void setSource(String source) {
        this.source = source;
    }

    public void setParent(Props prop) {
    	this._parent = prop;
    }
}