/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 gobblin.configuration;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import gobblin.compat.hadoop.TextSerializer;
import gobblin.compat.hadoop.WritableShim;
/**
* A serializable wrapper class that can be persisted for {@link Properties}.
*
* @author kgoodhop
*/
@EqualsAndHashCode(exclude = {"jsonParser"})
public class State implements WritableShim {
private static final Joiner LIST_JOINER = Joiner.on(",");
private static final Splitter LIST_SPLITTER = Splitter.on(",").trimResults().omitEmptyStrings();
private String id;
// State contains two parts: commonProperties and specProperties (specProperties overrides commonProperties).
@Getter
private Properties commonProperties;
@Getter
private Properties specProperties;
private final JsonParser jsonParser = new JsonParser();
public State() {
this.specProperties = new Properties();
this.commonProperties = new Properties();
}
public State(Properties properties) {
this.specProperties = properties;
this.commonProperties = new Properties();
}
public State(State otherState) {
this.commonProperties = otherState.getCommonProperties();
this.specProperties = new Properties();
this.specProperties.putAll(otherState.getProperties());
for (Object key : this.commonProperties.keySet()) {
if (this.specProperties.containsKey(key) && this.commonProperties.get(key).equals(this.specProperties.get(key))) {
this.specProperties.remove(key);
}
}
}
/**
* Return a copy of the underlying {@link Properties} object.
*
* @return A copy of the underlying {@link Properties} object.
*/
public Properties getProperties() {
// a.putAll(b) iterates over the entries of b. Synchronizing on b prevents concurrent modification on b.
synchronized (this.specProperties) {
Properties props = new Properties();
if (this.commonProperties != null) {
props.putAll(this.commonProperties);
}
props.putAll(this.specProperties);
return props;
}
}
/**
* Populates this instance with properties of the other instance.
*
* @param otherState the other {@link State} instance
*/
public void addAll(State otherState) {
Properties diffCommonProps = new Properties();
diffCommonProps.putAll(Maps.difference(this.commonProperties, otherState.commonProperties).entriesOnlyOnRight());
addAll(diffCommonProps);
addAll(otherState.specProperties);
}
/**
* Populates this instance with values of a {@link Properties} instance.
*
* @param properties a {@link Properties} instance
*/
public void addAll(Properties properties) {
this.specProperties.putAll(properties);
}
/**
* Add properties in a {@link State} instance that are not in the current instance.
*
* @param otherState a {@link State} instance
*/
public void addAllIfNotExist(State otherState) {
addAllIfNotExist(otherState.commonProperties);
addAllIfNotExist(otherState.specProperties);
}
/**
* Add properties in a {@link Properties} instance that are not in the current instance.
*
* @param properties a {@link Properties} instance
*/
public void addAllIfNotExist(Properties properties) {
for (String key : properties.stringPropertyNames()) {
if (!this.specProperties.containsKey(key) && !this.commonProperties.containsKey(key)) {
this.specProperties.setProperty(key, properties.getProperty(key));
}
}
}
/**
* Add properties in a {@link State} instance that are in the current instance.
*
* @param otherState a {@link State} instance
*/
public void overrideWith(State otherState) {
overrideWith(otherState.commonProperties);
overrideWith(otherState.specProperties);
}
/**
* Add properties in a {@link Properties} instance that are in the current instance.
*
* @param properties a {@link Properties} instance
*/
public void overrideWith(Properties properties) {
for (String key : properties.stringPropertyNames()) {
if (this.specProperties.containsKey(key) || this.commonProperties.containsKey(key)) {
this.specProperties.setProperty(key, properties.getProperty(key));
}
}
}
/**
* Set the id used for state persistence and logging.
*
* @param id id of this instance
*/
public void setId(String id) {
this.id = id;
}
/**
* Get the id of this instance.
*
* @return id of this instance
*/
public String getId() {
return this.id;
}
/**
* Set a property.
*
* <p>
* Both key and value are stored as strings.
* </p>
*
* @param key property key
* @param value property value
*/
public void setProp(String key, Object value) {
this.specProperties.put(key, value.toString());
}
/**
* Override existing {@link #commonProperties} and {@link #specProperties}.
* @param commonProperties
* @param specProperties
*/
public void setProps(Properties commonProperties, Properties specProperties) {
this.commonProperties = commonProperties;
this.specProperties = specProperties;
}
/**
* Appends the input value to a list property that can be retrieved with {@link #getPropAsList}.
*
* <p>
* List properties are internally stored as comma separated strings. Adding a value that contains commas (for
* example "a,b,c") will essentially add multiple values to the property ("a", "b", and "c"). This is
* similar to the way that {@link org.apache.hadoop.conf.Configuration} works.
* </p>
*
* @param key property key
* @param value property value (if it includes commas, it will be split by the commas).
*/
public synchronized void appendToListProp(String key, String value) {
if (contains(key)) {
setProp(key, LIST_JOINER.join(getProp(key), value));
} else {
setProp(key, value);
}
}
/**
* Appends the input value to a set property that can be retrieved with {@link #getPropAsSet}.
*
* <p>
* Set properties are internally stored as comma separated strings. Adding a value that contains commas (for
* example "a,b,c") will essentially add multiple values to the property ("a", "b", and "c"). This is
* similar to the way that {@link org.apache.hadoop.conf.Configuration} works.
* </p>
*
* @param key property key
* @param value property value (if it includes commas, it will be split by the commas).
*/
public synchronized void appendToSetProp(String key, String value) {
Set<String> set = value == null ? Sets.<String>newHashSet() : Sets.newHashSet(LIST_SPLITTER.splitToList(value));
if (contains(key)) {
set.addAll(getPropAsSet(key));
}
setProp(key, LIST_JOINER.join(set));
}
/**
* Get the value of a property.
*
* @param key property key
* @return value associated with the key as a string or <code>null</code> if the property is not set
*/
public String getProp(String key) {
if (this.specProperties.containsKey(key)) {
return this.specProperties.getProperty(key);
}
return this.commonProperties.getProperty(key);
}
/**
* Get the value of a property, using the given default value if the property is not set.
*
* @param key property key
* @param def default value
* @return value associated with the key or the default value if the property is not set
*/
public String getProp(String key, String def) {
if (this.specProperties.containsKey(key)) {
return this.specProperties.getProperty(key);
}
return this.commonProperties.getProperty(key, def);
}
/**
* Get the value of a comma separated property as a {@link List} of strings.
*
* @param key property key
* @return value associated with the key as a {@link List} of strings
*/
public List<String> getPropAsList(String key) {
return LIST_SPLITTER.splitToList(getProp(key));
}
/**
* Get the value of a property as a list of strings, using the given default value if the property is not set.
*
* @param key property key
* @param def default value
* @return value (the default value if the property is not set) associated with the key as a list of strings
*/
public List<String> getPropAsList(String key, String def) {
return LIST_SPLITTER.splitToList(getProp(key, def));
}
/**
* Get the value of a comma separated property as a {@link Set} of strings.
*
* @param key property key
* @return value associated with the key as a {@link Set} of strings
*/
public Set<String> getPropAsSet(String key) {
return ImmutableSet.copyOf(LIST_SPLITTER.splitToList(getProp(key)));
}
/**
* Get the value of a comma separated property as a {@link Set} of strings.
*
* @param key property key
* @param def default value
* @return value (the default value if the property is not set) associated with the key as a {@link Set} of strings
*/
public Set<String> getPropAsSet(String key, String def) {
return ImmutableSet.copyOf(LIST_SPLITTER.splitToList(getProp(key, def)));
}
/**
* Get the value of a property as a case insensitive {@link Set} of strings.
*
* @param key property key
* @return value associated with the key as a case insensitive {@link Set} of strings
*/
public Set<String> getPropAsCaseInsensitiveSet(String key) {
return ImmutableSortedSet.copyOf(String.CASE_INSENSITIVE_ORDER, LIST_SPLITTER.split(getProp(key)));
}
/**
* Get the value of a property as a case insensitive {@link Set} of strings, using the given default value if the property is not set.
*
* @param key property key
* @param def default value
* @return value associated with the key as a case insensitive {@link Set} of strings
*/
public Set<String> getPropAsCaseInsensitiveSet(String key, String def) {
return ImmutableSortedSet.copyOf(String.CASE_INSENSITIVE_ORDER, LIST_SPLITTER.split(getProp(key, def)));
}
/**
* Get the value of a property as a long integer.
*
* @param key property key
* @return long integer value associated with the key
*/
public long getPropAsLong(String key) {
return Long.parseLong(getProp(key));
}
/**
* Get the value of a property as a long integer, using the given default value if the property is not set.
*
* @param key property key
* @param def default value
* @return long integer value associated with the key or the default value if the property is not set
*/
public long getPropAsLong(String key, long def) {
return Long.parseLong(getProp(key, String.valueOf(def)));
}
/**
* Get the value of a property as an integer.
*
* @param key property key
* @return integer value associated with the key
*/
public int getPropAsInt(String key) {
return Integer.parseInt(getProp(key));
}
/**
* Get the value of a property as an integer, using the given default value if the property is not set.
*
* @param key property key
* @param def default value
* @return integer value associated with the key or the default value if the property is not set
*/
public int getPropAsInt(String key, int def) {
return Integer.parseInt(getProp(key, String.valueOf(def)));
}
/**
* Get the value of a property as a short.
*
* @param key property key
* @return short value associated with the key
*/
public short getPropAsShort(String key) {
return Short.parseShort(getProp(key));
}
/**
* Get the value of a property as a short.
*
* @param key property key
* @param radix radix used to parse the value
* @return short value associated with the key
*/
public short getPropAsShortWithRadix(String key, int radix) {
return Short.parseShort(getProp(key), radix);
}
/**
* Get the value of a property as an short, using the given default value if the property is not set.
*
* @param key property key
* @param def default value
* @return short value associated with the key or the default value if the property is not set
*/
public short getPropAsShort(String key, short def) {
return Short.parseShort(getProp(key, String.valueOf(def)));
}
/**
* Get the value of a property as an short, using the given default value if the property is not set.
*
* @param key property key
* @param def default value
* @param radix radix used to parse the value
* @return short value associated with the key or the default value if the property is not set
*/
public short getPropAsShortWithRadix(String key, short def, int radix) {
return contains(key) ? Short.parseShort(getProp(key), radix) : def;
}
/**
* Get the value of a property as a double.
*
* @param key property key
* @return double value associated with the key
*/
public double getPropAsDouble(String key) {
return Double.parseDouble(getProp(key));
}
/**
* Get the value of a property as a double, using the given default value if the property is not set.
*
* @param key property key
* @param def default value
* @return double value associated with the key or the default value if the property is not set
*/
public double getPropAsDouble(String key, double def) {
return Double.parseDouble(getProp(key, String.valueOf(def)));
}
/**
* Get the value of a property as a boolean.
*
* @param key property key
* @return boolean value associated with the key
*/
public boolean getPropAsBoolean(String key) {
return Boolean.parseBoolean(getProp(key));
}
/**
* Get the value of a property as a boolean, using the given default value if the property is not set.
*
* @param key property key
* @param def default value
* @return boolean value associated with the key or the default value if the property is not set
*/
public boolean getPropAsBoolean(String key, boolean def) {
return Boolean.parseBoolean(getProp(key, String.valueOf(def)));
}
/**
* Get the value of a property as a {@link JsonArray}.
*
* @param key property key
* @return {@link JsonArray} value associated with the key
*/
public JsonArray getPropAsJsonArray(String key) {
JsonElement jsonElement = this.jsonParser.parse(getProp(key));
Preconditions.checkArgument(jsonElement.isJsonArray(),
"Value for key " + key + " is malformed, it must be a JsonArray: " + jsonElement);
return jsonElement.getAsJsonArray();
}
/**
* Remove a property if it exists.
*
* @param key property key
*/
public void removeProp(String key) {
this.specProperties.remove(key);
if (this.commonProperties.containsKey(key)) {
// This case should not happen.
Properties commonPropsCopy = new Properties();
commonPropsCopy.putAll(this.commonProperties);
commonPropsCopy.remove(key);
this.commonProperties = commonPropsCopy;
}
}
/**
* @deprecated Use {@link #getProp(String)}
*/
@Deprecated
protected String getProperty(String key) {
return getProp(key);
}
/**
* @deprecated Use {@link #getProp(String, String)}
*/
@Deprecated
protected String getProperty(String key, String def) {
return getProp(key, def);
}
/**
* Get the names of all the properties set in a {@link Set}.
*
* @return names of all the properties set in a {@link Set}
*/
public Set<String> getPropertyNames() {
return Sets.newHashSet(
Iterables.concat(this.commonProperties.stringPropertyNames(), this.specProperties.stringPropertyNames()));
}
/**
* Check if a property is set.
*
* @param key property key
* @return <code>true</code> if the property is set or <code>false</code> otherwise
*/
public boolean contains(String key) {
return this.specProperties.containsKey(key) || this.commonProperties.containsKey(key);
}
@Override
public void readFields(DataInput in)
throws IOException {
int numEntries = in.readInt();
while (numEntries-- > 0) {
String key = TextSerializer.readTextAsString(in).intern();
String value = TextSerializer.readTextAsString(in).intern();
this.specProperties.put(key, value);
}
}
@Override
public void write(DataOutput out)
throws IOException {
out.writeInt(this.commonProperties.size() + this.specProperties.size());
for (Object key : this.commonProperties.keySet()) {
TextSerializer.writeStringAsText(out, (String) key);
TextSerializer.writeStringAsText(out, this.commonProperties.getProperty((String) key));
}
for (Object key : this.specProperties.keySet()) {
TextSerializer.writeStringAsText(out, (String) key);
TextSerializer.writeStringAsText(out, this.specProperties.getProperty((String) key));
}
}
@Override
public String toString() {
return "Common:" + this.commonProperties.toString() + "\n Specific: " + this.specProperties.toString();
}
}