/*
* Copyright 2012 LinkedIn Corp.
*
* 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.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
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.
*/
public Props(final Props parent) {
this._current = new HashMap<>();
this._parent = parent;
}
/**
* Load props from a file.
*/
public Props(final Props parent, final String filepath) throws IOException {
this(parent, new File(filepath));
}
/**
* Load props from a file.
*/
public Props(final Props parent, final File file) throws IOException {
this(parent);
setSource(file.getPath());
final InputStream input = new BufferedInputStream(new FileInputStream(file));
try {
loadFrom(input);
} catch (final IOException e) {
throw e;
} finally {
input.close();
}
}
/**
* Create props from property input streams
*/
public Props(final Props parent, final InputStream inputStream) throws IOException {
this(parent);
loadFrom(inputStream);
}
/**
* Create properties from maps of properties
*/
public Props(final Props parent, final Map<String, String>... props) {
this(parent);
for (int i = props.length - 1; i >= 0; i--) {
this.putAll(props[i]);
}
}
/**
* Create properties from Properties objects
*/
public Props(final Props parent, final 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.
*/
public Props(final Props parent, final 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 ...]
*/
public static Props of(final String... args) {
return of((Props) null, args);
}
/**
* Create a Props from a list of key value pairing. i.e. [key1, value1, key2, value2 ...]
*/
public static Props of(final Props parent, final String... args) {
if (args.length % 2 != 0) {
throw new IllegalArgumentException(
"Must have an equal number of keys and values.");
}
final Map<String, String> vals = new HashMap<>(args.length / 2);
for (int i = 0; i < args.length; i += 2) {
vals.put(args[i], args[i + 1]);
}
return new Props(parent, vals);
}
/**
* Clones the Props p object and all of its parents.
*/
public static Props clone(final Props p) {
return copyNext(p);
}
/**
*
* @param source
* @return
*/
private static Props copyNext(final Props source) {
Props priorNodeCopy = null;
if (source.getParent() != null) {
priorNodeCopy = copyNext(source.getParent());
}
final Props dest = new Props(priorNodeCopy);
for (final String key : source.localKeySet()) {
dest.put(key, source.get(key));
}
return dest;
}
/**
*
* @param inputStream
* @throws IOException
*/
private void loadFrom(final InputStream inputStream) throws IOException {
final Properties properties = new Properties();
properties.load(inputStream);
this.put(properties);
}
public Props getEarliestAncestor() {
if (this._parent == null) {
return this;
}
return this._parent.getEarliestAncestor();
}
public void setEarliestAncestor(final Props parent) {
final Props props = getEarliestAncestor();
props.setParent(parent);
}
/**
* Clear the current Props, but leaves the parent untouched.
*/
public void clearLocal() {
this._current.clear();
}
/**
* Check key in current Props then search in parent
*/
public boolean containsKey(final Object k) {
return this._current.containsKey(k)
|| (this._parent != null && this._parent.containsKey(k));
}
/**
* Check value in current Props then search in parent
*/
public boolean containsValue(final Object value) {
return this._current.containsValue(value)
|| (this._parent != null && this._parent.containsValue(value));
}
/**
* Return value if available in current Props otherwise return from parent
*/
public String get(final Object key) {
if (this._current.containsKey(key)) {
return this._current.get(key);
} else if (this._parent != null) {
return this._parent.get(key);
} else {
return null;
}
}
/**
* Get the key set from the current Props
*/
public Set<String> localKeySet() {
return this._current.keySet();
}
/**
* Get parent Props
*/
public Props getParent() {
return this._parent;
}
public void setParent(final Props prop) {
this._parent = prop;
}
/**
* 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(final String key, final String value) {
return this._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(final Properties properties) {
for (final String propName : properties.stringPropertyNames()) {
this._current.put(propName, properties.getProperty(propName));
}
}
/**
* Put integer
*/
public String put(final String key, final Integer value) {
return this._current.put(key, value.toString());
}
/**
* Put Long. Stores as String.
*/
public String put(final String key, final Long value) {
return this._current.put(key, value.toString());
}
/**
* Put Double. Stores as String.
*/
public String put(final String key, final Double value) {
return this._current.put(key, value.toString());
}
/**
* Put everything in the map into the props.
*/
public void putAll(final Map<? extends String, ? extends String> m) {
if (m == null) {
return;
}
for (final 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.
*/
public void putAll(final Props p) {
if (p == null) {
return;
}
for (final String key : p.getKeySet()) {
this.put(key, p.get(key));
}
}
/**
* Puts only the local props from p into the current properties
*/
public void putLocal(final Props p) {
for (final String key : p.localKeySet()) {
this.put(key, p.get(key));
}
}
/**
* Remove only the local value of key s, and not the parents.
*/
public String removeLocal(final Object s) {
return this._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 this._current.size();
}
/**
* Attempts to return the Class that corresponds to the Props value. If the class doesn't exit, an
* IllegalArgumentException will be thrown.
*/
public Class<?> getClass(final String key) {
try {
if (containsKey(key)) {
return Class.forName(get(key));
} else {
throw new UndefinedPropertyException("Missing required property '"
+ key + "'");
}
} catch (final ClassNotFoundException e) {
throw new IllegalArgumentException(e);
}
}
public Class<?> getClass(final String key, final boolean initialize, final ClassLoader cl) {
try {
if (containsKey(key)) {
return Class.forName(get(key), initialize, cl);
} else {
throw new UndefinedPropertyException("Missing required property '"
+ key + "'");
}
} catch (final ClassNotFoundException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Gets the class from the Props. If it doesn't exist, it will return the defaultClass
*/
public Class<?> getClass(final String key, final 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
*/
public String getString(final String key, final 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
*/
public String getString(final 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
*/
public List<String> getStringList(final String key) {
return getStringList(key, "\\s*,\\s*");
}
/**
* Returns a list of clusters with the comma as the separator of the value
* e.g., for input string: "thrift://hcat1:port,thrift://hcat2:port;thrift://hcat3:port,thrift://hcat4:port;"
* we will get ["thrift://hcat1:port,thrift://hcat2:port", "thrift://hcat3:port,thrift://hcat4:port"] as output
*/
public List<String> getStringListFromCluster(final String key) {
List<String> curlist = getStringList(key, "\\s*;\\s*");
// remove empty elements in the array
for (Iterator<String> iter = curlist.listIterator(); iter.hasNext(); ) {
String a = iter.next();
if (a.length() == 0) {
iter.remove();
}
}
return curlist;
}
/**
* Returns a list of strings with the sep as the separator of the value
*/
public List<String> getStringList(final String key, final String sep) {
final 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.
*/
public List<String> getStringList(final String key, final 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.
*/
public List<String> getStringList(final String key, final List<String> defaultValue,
final 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.
*/
public boolean getBoolean(final String key, final 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.
*/
public boolean getBoolean(final 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.
*/
public long getLong(final String name, final 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.
*/
public long getLong(final 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.
*/
public int getInt(final String name, final 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.
*/
public int getInt(final 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.
*/
public double getDouble(final String name, final 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.
*/
public double getDouble(final 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.
*/
public URI getUri(final String name) {
if (containsKey(name)) {
try {
return new URI(get(name));
} catch (final 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.
*/
public URI getUri(final String name, final URI defaultValue) {
if (containsKey(name)) {
return getUri(name);
} else {
return defaultValue;
}
}
public URI getUri(final String name, final String defaultValue) {
try {
return getUri(name, new URI(defaultValue));
} catch (final 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(final File file) throws IOException {
final BufferedOutputStream out =
new BufferedOutputStream(new FileOutputStream(file));
try {
storeLocal(out);
} finally {
out.close();
}
}
/**
* Returns a copy of only the local values of this props
*/
public Props local() {
return new Props(null, this._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(final OutputStream out) throws IOException {
final Properties p = new Properties();
for (final String key : this._current.keySet()) {
p.setProperty(key, get(key));
}
p.store(out, null);
}
/**
* Returns a java.util.Properties file populated with the stuff in here.
*/
public Properties toProperties() {
final Properties p = new Properties();
for (final String key : this._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(final File file) throws IOException {
final 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(final OutputStream out) throws IOException {
final Properties p = new Properties();
for (Props curr = this; curr != null; curr = curr.getParent()) {
for (final String key : curr.localKeySet()) {
if (!p.containsKey(key)) {
p.setProperty(key, get(key));
}
}
}
p.store(out, null);
}
/**
* Returns a map of all the flattened properties, the item in the returned map is sorted
* alphabetically by the key value.
*
* @Return
*/
public Map<String, String> getFlattened() {
final TreeMap<String, String> returnVal = new TreeMap<>();
returnVal.putAll(getMapByPrefix(""));
return returnVal;
}
/**
* Get a map of all properties by string prefix
*
* @param prefix The string prefix
*/
public Map<String, String> getMapByPrefix(final String prefix) {
final Map<String, String> values = this._parent == null ? new HashMap<>() :
this._parent.getMapByPrefix(prefix);
// when there is a conflict, value from the child takes the priority.
for (final 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
*/
public Set<String> getKeySet() {
final HashSet<String> keySet = new HashSet<>();
keySet.addAll(localKeySet());
if (this._parent != null) {
keySet.addAll(this._parent.getKeySet());
}
return keySet;
}
/**
* Logs the property in the given logger
*/
public void logProperties(final Logger logger, final String comment) {
logger.info(comment);
for (final String key : getKeySet()) {
logger.info(" key=" + key + " value=" + get(key));
}
}
/**
*/
@Override
public boolean equals(final Object o) {
if (o == this) {
return true;
} else if (o == null) {
return false;
} else if (o.getClass() != Props.class) {
return false;
}
final Props p = (Props) o;
return this._current.equals(p._current) && Utils.equals(this._parent, p._parent);
}
/**
* Returns true if the properties are equivalent, regardless of the hierarchy.
*/
public boolean equalsProps(final Props p) {
if (p == null) {
return false;
}
final Set<String> myKeySet = getKeySet();
for (final 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 (this._parent != null) {
code += this._parent.hashCode();
}
return code;
}
/**
*
*/
@Override
public String toString() {
final StringBuilder builder = new StringBuilder("{");
for (final Map.Entry<String, String> entry : this._current.entrySet()) {
builder.append(entry.getKey());
builder.append(": ");
builder.append(entry.getValue());
builder.append(", ");
}
if (this._parent != null) {
builder.append(" parent = ");
builder.append(this._parent.toString());
}
builder.append("}");
return builder.toString();
}
public String getSource() {
return this.source;
}
public void setSource(final String source) {
this.source = source;
}
}