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