blob: 7508025f0c25d9827ba24c8c7d96be906017ff96 [file] [log] [blame]
/*
* 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 org.apache.gobblin.util;
import com.google.common.base.Preconditions;
import java.io.IOException;
import java.io.StringReader;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
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.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.opencsv.CSVReader;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigList;
import com.typesafe.config.ConfigValue;
import org.apache.gobblin.configuration.State;
import org.apache.gobblin.password.PasswordManager;
/**
* Utility class for dealing with {@link Config} objects.
*/
public class ConfigUtils {
private final FileUtils fileUtils;
/**
* List of keys that should be excluded when converting to typesafe config.
* Usually, it is the key that is both the parent object of a value and a value, which is disallowed by Typesafe.
*/
private static final String GOBBLIN_CONFIG_BLACKLIST_KEYS = "gobblin.config.blacklistKeys";
/**
* A suffix that is automatically appended to property keys that are prefixes of other
* property keys. This is used during Properties -> Config -> Properties conversion since
* typesafe config does not allow such properties. */
public static final String STRIP_SUFFIX = ".ROOT_VALUE";
/**
* Available TimeUnit values that can be parsed from a given String
*/
private static final Set<String> validTimeUnits = Arrays.stream(TimeUnit.values())
.map(TimeUnit::name)
.collect(Collectors.toSet());
public ConfigUtils(FileUtils fileUtils) {
this.fileUtils = fileUtils;
}
public void saveConfigToFile(final Config config, final Path destPath)
throws IOException {
final String configAsHoconString = config.root().render();
this.fileUtils.saveToFile(configAsHoconString, destPath);
}
/**
* Convert a given {@link Config} instance to a {@link Properties} instance.
*
* @param config the given {@link Config} instance
* @return a {@link Properties} instance
*/
public static Properties configToProperties(Config config) {
return configToProperties(config, Optional.absent());
}
/**
* Convert a given {@link Config} instance to a {@link Properties} instance.
* If the config value is not of String type, it will try to get it as a generic Object type
* using {@see com.typesafe.config.Config#getAnyRef()} and then try to return its json representation as a string
*
* @param config the given {@link Config} instance
* @param prefix an optional prefix; if present, only properties whose name starts with the prefix
* will be returned.
* @return a {@link Properties} instance
*/
public static Properties configToProperties(Config config, Optional<String> prefix) {
Properties properties = new Properties();
if (config != null) {
Config resolvedConfig = config.resolve();
for (Map.Entry<String, ConfigValue> entry : resolvedConfig.entrySet()) {
if (!prefix.isPresent() || entry.getKey().startsWith(prefix.get())) {
String propKey = desanitizeKey(entry.getKey());
String propVal;
try {
propVal = resolvedConfig.getString(entry.getKey());
} catch (ConfigException.WrongType wrongType) {
propVal = new Gson().toJson(resolvedConfig.getAnyRef(entry.getKey()));
}
properties.setProperty(propKey, propVal);
}
}
}
return properties;
}
/**
* Convert a given {@link Config} instance to a {@link Properties} instance.
*
* @param config the given {@link Config} instance
* @param prefix only properties whose name starts with the prefix will be returned.
* @return a {@link Properties} instance
*/
public static Properties configToProperties(Config config, String prefix) {
return configToProperties(config, Optional.of(prefix));
}
/**
* @return the subconfig under key "key" if it exists, otherwise returns an empty config.
*/
public static Config getConfigOrEmpty(Config config, String key) {
try {
if (config.hasPath(key)) {
return config.getConfig(key);
} else {
return ConfigFactory.empty();
}
} catch (ConfigException.WrongType wrongType) {
// path exists, but it is not a subconfig
return ConfigFactory.empty();
}
}
/**
* Convert a given {@link Config} to a {@link State} instance.
*
* @param config the given {@link Config} instance
* @return a {@link State} instance
*/
public static State configToState(Config config) {
return new State(configToProperties(config));
}
/**
* Convert a given {@link Properties} to a {@link Config} instance.
*
* <p>
* This method will throw an exception if (1) the {@link Object#toString()} method of any two keys in the
* {@link Properties} objects returns the same {@link String}, or (2) if any two keys are prefixes of one another,
* see the Java Docs of {@link ConfigFactory#parseMap(Map)} for more details.
* </p>
*
* @param properties the given {@link Properties} instance
* @return a {@link Config} instance
*/
public static Config propertiesToConfig(Properties properties) {
return propertiesToConfig(properties, Optional.absent());
}
/**
* Finds a list of properties whose keys are complete prefix of other keys. This function is
* meant to be used during conversion from Properties to typesafe Config as the latter does not
* support this scenario.
* @param properties the Properties collection to inspect
* @param keyPrefix an optional key prefix which limits which properties are inspected.
* */
public static Set<String> findFullPrefixKeys(Properties properties,
Optional<String> keyPrefix) {
TreeSet<String> propNames = new TreeSet<>();
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
String entryKey = entry.getKey().toString();
if (StringUtils.startsWith(entryKey, keyPrefix.or(StringUtils.EMPTY))) {
propNames.add(entryKey);
}
}
Set<String> result = new HashSet<>();
String lastKey = null;
Iterator<String> sortedKeysIter = propNames.iterator();
while(sortedKeysIter.hasNext()) {
String propName = sortedKeysIter.next();
if (null != lastKey && propName.startsWith(lastKey + ".")) {
result.add(lastKey);
}
lastKey = propName;
}
return result;
}
/**
* Convert all the keys that start with a <code>prefix</code> in {@link Properties} to a {@link Config} instance.
*
* <p>
* This method will throw an exception if (1) the {@link Object#toString()} method of any two keys in the
* {@link Properties} objects returns the same {@link String}, or (2) if any two keys are prefixes of one another,
* see the Java Docs of {@link ConfigFactory#parseMap(Map)} for more details.
* </p>
*
* @param properties the given {@link Properties} instance
* @param prefix of keys to be converted
* @return a {@link Config} instance
*/
public static Config propertiesToConfig(Properties properties, Optional<String> prefix) {
Set<String> blacklistedKeys = new HashSet<>(0);
if (properties.containsKey(GOBBLIN_CONFIG_BLACKLIST_KEYS)) {
blacklistedKeys = new HashSet<>(Splitter.on(',').omitEmptyStrings().trimResults()
.splitToList(properties.getProperty(GOBBLIN_CONFIG_BLACKLIST_KEYS)));
}
Set<String> fullPrefixKeys = findFullPrefixKeys(properties, prefix);
ImmutableMap.Builder<String, Object> immutableMapBuilder = ImmutableMap.builder();
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
String entryKey = entry.getKey().toString();
if (StringUtils.startsWith(entryKey, prefix.or(StringUtils.EMPTY)) &&
!blacklistedKeys.contains(entryKey)) {
if (fullPrefixKeys.contains(entryKey)) {
entryKey = sanitizeFullPrefixKey(entryKey);
} else if (sanitizedKey(entryKey)) {
throw new RuntimeException("Properties are not allowed to end in " + STRIP_SUFFIX);
}
immutableMapBuilder.put(entryKey, entry.getValue());
}
}
return ConfigFactory.parseMap(immutableMapBuilder.build());
}
public static String sanitizeFullPrefixKey(String propKey) {
return propKey + STRIP_SUFFIX;
}
/**
* returns true if is it a sanitized key
*/
public static boolean sanitizedKey(String propKey) {
return propKey.endsWith(STRIP_SUFFIX);
}
public static String desanitizeKey(String propKey) {
propKey = sanitizedKey(propKey) ?
propKey.substring(0, propKey.length() - STRIP_SUFFIX.length()) : propKey;
// Also strip quotes that can get introduced by TypeSafe.Config
propKey = propKey.replace("\"", "");
return propKey;
}
/**
* Convert all the keys that start with a <code>prefix</code> in {@link Properties} to a
* {@link Config} instance. The method also tries to guess the types of properties.
*
* <p>
* This method will throw an exception if (1) the {@link Object#toString()} method of any two keys in the
* {@link Properties} objects returns the same {@link String}, or (2) if any two keys are prefixes of one another,
* see the Java Docs of {@link ConfigFactory#parseMap(Map)} for more details.
* </p>
*
* @param properties the given {@link Properties} instance
* @param prefix of keys to be converted
* @return a {@link Config} instance
*/
public static Config propertiesToTypedConfig(Properties properties, Optional<String> prefix) {
Map<String, Object> typedProps = guessPropertiesTypes(properties);
ImmutableMap.Builder<String, Object> immutableMapBuilder = ImmutableMap.builder();
for (Map.Entry<String, Object> entry : typedProps.entrySet()) {
if (StringUtils.startsWith(entry.getKey(), prefix.or(StringUtils.EMPTY))) {
immutableMapBuilder.put(entry.getKey(), entry.getValue());
}
}
return ConfigFactory.parseMap(immutableMapBuilder.build());
}
/** Attempts to guess type types of a Properties. By default, typesafe will make all property
* values Strings. This implementation will try to recognize booleans and numbers. All keys are
* treated as strings.*/
private static Map<String, Object> guessPropertiesTypes(Map<Object, Object> srcProperties) {
Map<String, Object> res = Maps.newHashMapWithExpectedSize(srcProperties.size());
for (Map.Entry<Object, Object> prop : srcProperties.entrySet()) {
Object value = prop.getValue();
if (null != value && value instanceof String && !Strings.isNullOrEmpty(value.toString())) {
try {
value = Long.parseLong(value.toString());
} catch (NumberFormatException e) {
try {
value = Double.parseDouble(value.toString());
} catch (NumberFormatException e2) {
if (value.toString().equalsIgnoreCase("true") || value.toString().equalsIgnoreCase("yes")) {
value = Boolean.TRUE;
} else if (value.toString().equalsIgnoreCase("false") || value.toString().equalsIgnoreCase("no")) {
value = Boolean.FALSE;
} else {
// nothing to do
}
}
}
}
res.put(prop.getKey().toString(), value);
}
return res;
}
/**
* Return string value at <code>path</code> if <code>config</code> has path. If not return an empty string
*
* @param config in which the path may be present
* @param path key to look for in the config object
* @return string value at <code>path</code> if <code>config</code> has path. If not return an empty string
*/
public static String emptyIfNotPresent(Config config, String path) {
return getString(config, path, StringUtils.EMPTY);
}
/**
* Return string value at <code>path</code> if <code>config</code> has path. If not return <code>def</code>
* If the config value is not of String type, it will try to get it as a generic Object type
* using {@see com.typesafe.config.Config#getAnyRef()} and then try to return its json representation as a string
* @param config in which the path may be present
* @param path key to look for in the config object
* @return string value at <code>path</code> if <code>config</code> has path. If not return <code>def</code>
*/
public static String getString(Config config, String path, String def) {
if (config.hasPath(path)) {
String value;
try {
value = config.getString(path);
} catch (ConfigException.WrongType wrongType) {
value = new Gson().toJson(config.getAnyRef(path));
}
return value;
}
return def;
}
/**
* Return TimeUnit value at <code>path</code> if <code>config</code> has path. If not return <code>def</code>
*
* @param config in which the path may be present
* @param path key to look for in the config object
* @return TimeUnit value at <code>path</code> if <code>config</code> has path. If not return <code>def</code>
*/
public static TimeUnit getTimeUnit(Config config, String path, TimeUnit def) {
if (config.hasPath(path)) {
String timeUnit = config.getString(path).toUpperCase();
Preconditions.checkArgument(validTimeUnits.contains(timeUnit),
"Passed invalid TimeUnit for documentTTLUnits: '%s'".format(timeUnit));
return TimeUnit.valueOf(timeUnit);
}
return def;
}
/**
* Return {@link Long} value at <code>path</code> if <code>config</code> has path. If not return <code>def</code>
*
* @param config in which the path may be present
* @param path key to look for in the config object
* @return {@link Long} value at <code>path</code> if <code>config</code> has path. If not return <code>def</code>
*/
public static Long getLong(Config config, String path, Long def) {
if (config.hasPath(path)) {
return Long.valueOf(config.getLong(path));
}
return def;
}
/**
* Return {@link Integer} value at <code>path</code> if <code>config</code> has path. If not return <code>def</code>
*
* @param config in which the path may be present
* @param path key to look for in the config object
* @return {@link Integer} value at <code>path</code> if <code>config</code> has path. If not return <code>def</code>
*/
public static Integer getInt(Config config, String path, Integer def) {
if (config.hasPath(path)) {
return Integer.valueOf(config.getInt(path));
}
return def;
}
/**
* Return boolean value at <code>path</code> if <code>config</code> has path. If not return <code>def</code>
*
* @param config in which the path may be present
* @param path key to look for in the config object
* @return boolean value at <code>path</code> if <code>config</code> has path. If not return <code>def</code>
*/
public static boolean getBoolean(Config config, String path, boolean def) {
if (config.hasPath(path)) {
return config.getBoolean(path);
}
return def;
}
/**
* Return double value at <code>path</code> if <code>config</code> has path. If not return <code>def</code>
*
* @param config in which the path may be present
* @param path key to look for in the config object
* @return double value at <code>path</code> if <code>config</code> has path. If not return <code>def</code>
*/
public static double getDouble(Config config, String path, double def) {
if (config.hasPath(path)) {
return config.getDouble(path);
}
return def;
}
/**
* Return {@link Config} value at <code>path</code> if <code>config</code> has path. If not return <code>def</code>
*
* @param config in which the path may be present
* @param path key to look for in the config object
* @return config value at <code>path</code> if <code>config</code> has path. If not return <code>def</code>
*/
public static Config getConfig(Config config, String path, Config def) {
if (config.hasPath(path)) {
return config.getConfig(path);
}
return def;
}
/**
* <p>
* An extension to {@link Config#getStringList(String)}. The value at <code>path</code> can either be a TypeSafe
* {@link ConfigList} of strings in which case it delegates to {@link Config#getStringList(String)} or as list of
* comma separated strings in which case it splits the comma separated list.
*
*
* </p>
* Additionally
* <li>Returns an empty list if <code>path</code> does not exist
* <li>removes any leading and lagging quotes from each string in the returned list.
*
* Examples below will all return a list [1,2,3] without quotes
*
* <ul>
* <li> a.b=[1,2,3]
* <li> a.b=["1","2","3"]
* <li> a.b=1,2,3
* <li> a.b="1","2","3"
* </ul>
*
* @param config in which the path may be present
* @param path key to look for in the config object
* @return list of strings
*/
public static List<String> getStringList(Config config, String path) {
if (!config.hasPath(path)) {
return Collections.emptyList();
}
List<String> valueList;
try {
valueList = config.getStringList(path);
} catch (ConfigException.WrongType e) {
if (StringUtils.isEmpty(config.getString(path))) {
return Collections.emptyList();
}
/*
* Using CSV Reader as values could be quoted.
* E.g The string "a","false","b","10,12" will be split to a list of 4 elements and not 5.
*
* a
* false
* b
* 10,12
*/
try (CSVReader csvr = new CSVReader(new StringReader(config.getString(path)))) {
valueList = Lists.newArrayList(csvr.readNext());
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
// Remove any leading or lagging quotes in the values
// [\"a\",\"b\"] ---> [a,b]
return Lists.newArrayList(Lists.transform(valueList, new Function<String, String>() {
@Override
public String apply(String input) {
if (input == null) {
return input;
}
return input.replaceAll("^\"|\"$", "");
}
}));
}
/**
* Check if the given <code>key</code> exists in <code>config</code> and it is not null or empty
* Uses {@link StringUtils#isNotBlank(CharSequence)}
* @param config which may have the key
* @param key to look for in the config
*
* @return True if key exits and not null or empty. False otherwise
*/
public static boolean hasNonEmptyPath(Config config, String key) {
return config.hasPath(key) && StringUtils.isNotBlank(config.getString(key));
}
/**
* Check that every key-value in superConfig is in subConfig
*/
public static boolean verifySubset(Config superConfig, Config subConfig) {
for (Map.Entry<String, ConfigValue> entry : subConfig.entrySet()) {
if (!superConfig.hasPath(entry.getKey()) || !superConfig.getValue(entry.getKey()).unwrapped()
.equals(entry.getValue().unwrapped())) {
return false;
}
}
return true;
}
/**
* Resolves encrypted config value(s) by considering on the path with "encConfigPath" as encrypted.
* (If encConfigPath is absent or encConfigPath does not exist in config, config will be just returned untouched.)
* It will use Password manager via given config. Thus, convention of PasswordManager need to be followed in order to be decrypted.
* Note that "encConfigPath" path will be removed from the config key, leaving child path on the config key.
* e.g:
* encConfigPath = enc.conf
* - Before : { enc.conf.secret_key : ENC(rOF43721f0pZqAXg#63a) }
* - After : { secret_key : decrypted_val }
*
* @param config
* @param encConfigPath
* @return
*/
public static Config resolveEncrypted(Config config, Optional<String> encConfigPath) {
if (!encConfigPath.isPresent() || !config.hasPath(encConfigPath.get())) {
return config;
}
Config encryptedConfig = config.getConfig(encConfigPath.get());
PasswordManager passwordManager = PasswordManager.getInstance(configToProperties(config));
Map<String, String> tmpMap = Maps.newHashMapWithExpectedSize(encryptedConfig.entrySet().size());
for (Map.Entry<String, ConfigValue> entry : encryptedConfig.entrySet()) {
String val = entry.getValue().unwrapped().toString();
val = passwordManager.readPassword(val);
tmpMap.put(entry.getKey(), val);
}
return ConfigFactory.parseMap(tmpMap).withFallback(config);
}
}