ch.inftec.ju.util.PropertyChainBuilder Maven / Gradle / Ivy
package ch.inftec.ju.util;
import java.io.BufferedReader;
import java.net.URL;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.plexus.interpolation.AbstractValueSource;
import org.codehaus.plexus.interpolation.EnvarBasedValueSource;
import org.codehaus.plexus.interpolation.InterpolationException;
import org.codehaus.plexus.interpolation.Interpolator;
import org.codehaus.plexus.interpolation.RegexBasedInterpolator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.inftec.ju.security.JuSecurityUtils;
import ch.inftec.ju.security.JuTextEncryptor;
import ch.inftec.ju.util.PropertyChain.PropertyInfo;
/**
* Builder to create PropertyChain instances.
*
* A PropertyChain can evaluate properties using multiple PropertyEvaluators that can
* be arranged in a chain, priorizing the property evaluation by the order they are added to
* the chain.
*
* The PropertyChain can also be configured to allow interpolation, e.g. to replace ${name} with the value
* of the property 'name'.
* @author Martin
*
*/
public class PropertyChainBuilder {
private Logger logger = LoggerFactory.getLogger(PropertyChainBuilder.class);
/**
* List of folders on file system we will search for resources.
*/
private final List resourceFolders = new ArrayList<>();
private final List evaluators = new ArrayList<>();
private InterpolationBuilder interpolationBuilder = new InterpolationBuilder();
/**
* Set of values we shouldn't display values of in log (e.g. sensitive data like
* passwords).
*/
private final Set hiddenValueKeys = new HashSet<>();
/**
* String that will be logged for an encrypted value.
*/
private static final String ENCRYPTED_VALUE_LOGGING_STRING = "***";
/**
* TextEncryptor instance to decrypt encrypted texts.
*/
private JuTextEncryptor decryptor;
// Attributes of the PropertyChain
private boolean defaultThrowExceptionIfUndefined = false;
/**
* Specifies that the value for this key should not be displayed plainly. Can be used
* to protect sensitive data like passwords.
* @param key Key
* @return This builder to allow for chaining
*/
public PropertyChainBuilder hideValueForKey(String key) {
this.hiddenValueKeys.add(key);
return this;
}
/**
* Adds an evaluator that evaluates system properties.
* @return This builder to allow for chaining
*/
public PropertyChainBuilder addSystemPropertyEvaluator() {
return this.addPropertyEvaluator(new SystemPropertyEvaluator());
}
/**
* Adds a resource folder we will use for resource lookup.
*
* Note that the folder must be added BEFORE the corresponding resource is configured using
* the builder.
*
* All resource folders will be scanned before we look for the resource on the classpath
*
* If the folder doesn't exist or is not a folder, it will be ignored issuing a warning log.
* @param resourceFolder Resource folder to look for resource on file system
* @return This builder to allow for chaining
*/
public PropertyChainBuilder addResourceFolder(Path resourceFolder) {
if (JuUrl.path().isExistingDirectory(resourceFolder)) {
this.resourceFolders.add(resourceFolder);
} else {
logger.warn("Not an existing directory. Folder will be ignored: {}", resourceFolder.toAbsolutePath());
}
return this;
}
/**
* Adds an evaluator that reads properties from property files.
* @param resourceUrl URL to property file resource
* @return This builder to allow for chaining
*/
public PropertyChainBuilder addResourcePropertyEvaluator(URL resourceUrl) {
try {
return this.addPropertyEvaluator(new PropertiesPropertyEvaluator(resourceUrl));
} catch (JuException ex) {
throw new JuRuntimeException("Couldn't load properties from url " + resourceUrl, ex);
}
}
/**
* Adds an evaluator that reads properties from a property file.
* @param resourceName Name of the resource
* @param ignoreMissingResource If true, no evalautor will be added and no exception will
* be thrown if the resource doesn't exist
* @return This builder to allow for chaining
*/
public PropertyChainBuilder addResourcePropertyEvaluator(String resourceName, boolean ignoreMissingResource) {
try {
URL resourceUrl = JuUrl.resource(resourceName);
return this.addPropertyEvaluator(new PropertiesPropertyEvaluator(resourceUrl));
} catch (JuException ex) {
if (ignoreMissingResource) {
logger.debug(String.format("Ignoring missing resource %s (Exception: %s)", resourceName, ex.getMessage()));
return this;
} else {
throw new JuRuntimeException("Couldn't load properties from resource " + resourceName, ex);
}
}
}
/**
* Adds an evaluator that reads properties from the specified Properties object.
* @param props Properties instance
* @return This builder to allow for chaining
*/
public PropertyChainBuilder addPropertiesPropertyEvaluator(Properties props) {
return this.addPropertyEvaluator(new PropertiesPropertyEvaluator(props));
}
/**
* Adds an evaluator that will evalute from the static list of key/value pairs provided.
*
* Each even index is a key and each odd keyIndex+1 is it's value.
* @param keyValuePairs List of key value pairs
* @return This builder to allow for chaining
*/
public PropertyChainBuilder addListPropertyEvaluator(String... keyValuePairs) {
Properties props = new Properties();
for (int i = 0; i + 1 < keyValuePairs.length; i += 2) {
props.put(keyValuePairs[i], keyValuePairs[i + 1]);
}
return this.addPropertiesPropertyEvaluator(props);
}
/**
* Adds a PropertyChain as nested PropertyEvaluator.
* @param chain PropertyChain
* @return This builder to allow for chaining
*/
public PropertyChainBuilder addPropertyChainPropertyEvaluator(PropertyChain chain) {
return this.addPropertyEvaluator(new PropertyChainPropertyEvaluator(chain));
}
/**
* Adds an evaluator that reads properties from a CSV (comma separated value) file.
*
* The profile name will map to the column we want to use.
* @param resourceUrl Resource URL to the csv resource
* @param profileName Name of the profile (i.e. column identified by it's header / first row) to be used
* @param defaultColumnName Name of the column that contains default values if a value is not defined in the profile column
* @return
*/
public PropertyChainBuilder addCsvPropertyEvaluator(URL resourceUrl, String profileName, String defaultColumnName) {
CsvPropertyEvaluator csvEvaluator = new CsvPropertyEvaluator(resourceUrl, profileName, defaultColumnName);
return this.addPropertyEvaluator(csvEvaluator);
}
/**
* Adds a custom implementation of a PropertyEvaluator.
* @param evaluator PropertyEvaluator implementation
* @return This builder to allow for chaining
*/
public PropertyChainBuilder addPropertyEvaluator(PropertyEvaluator evaluator) {
this.evaluators.add(evaluator);
return this;
}
/**
* Sets the default exception throwing behaviour if a property is undefined.
*
* Initial value is false, i.e. no exceptions are thrown if a property is undefined and null
* is returned.
* @param defaultThrowExceptionIfUndefined True if by default, an exception should be thrown if a property is undefined
* @return This builder to allow for chaining
*/
public PropertyChainBuilder setDefaultThrowExceptionIfUndefined(boolean defaultThrowExceptionIfUndefined) {
this.defaultThrowExceptionIfUndefined = defaultThrowExceptionIfUndefined;
return this;
}
/**
* Sets a TextEncryptor to decrypt encrypted text like ENC(xxxx).
*
* Use the {@link ch.inftec.ju.security.JuSecurityUtils} to build an encryptor.
* @param decryptor TextEncryptor
* @return This builder to allow for chaining
*/
public PropertyChainBuilder setDecryptor(JuTextEncryptor decryptor) {
this.decryptor = decryptor;
return this;
}
/**
* Sets a decryptor for encrypted text like ENC(xxxx) by resolving the password from a resource.
*
* The resource will be lookup up using the chain builders resource folders first and then the classpath.
* @param resourceName Resource name containing the encryption password
* @return This builder to allow for chaining
*/
public PropertyChainBuilder setDecryptorByResource(String passwordResourceName, boolean strongEncryption) {
URL resourceUrl = this.getFileResource(passwordResourceName, false);
this.setDecryptor(JuSecurityUtils.buildEncryptor()
.passwordByUrl(resourceUrl)
.strong(strongEncryption)
.createTextEncryptor());
return this;
}
/**
* Returns an InterpolationBuilder to configure interpolation.
*
* By default, interpolation is turned on.
* @return InterpolationBuilder
*/
public InterpolationBuilder interpolation() {
return this.interpolationBuilder;
}
/**
* Gets a PropertyChain to peek at properties, i.e. evaluate them using the chain as configured so far.
*
* Note that this method will have the throwExceptionIfUndefined property set to false, regardless of the setting of
* throwExceptionIfUndefined of the builder.
* @return PropertyChain to peek at properties
*/
public InterpolatingPropertyChain peekChain() {
return new PropertyChainImpl(this, false);
}
/**
* Returns a ChainFilesResolver to add evaluators by chain files that contain
* chain informations.
*
* See ju-util/ju.properties.files
for a reference on chain files.
* @return ChainFilesResolver
*/
public ChainFilesResolver addEvaluatorsByChainFiles() {
return new ChainFilesResolver();
}
/**
* Gets the PropertyChain that was built using this builder.
*
* This property chain will support interpolation (though it has to be enabled
* to actually interpolate...
* @return PropertyChain instance
*/
public InterpolatingPropertyChain getPropertyChain() {
return new PropertyChainImpl(this, this.defaultThrowExceptionIfUndefined);
}
/**
* Helper class to evaluate chains by chain files.
* @author [email protected]
*
*/
public final class ChainFilesResolver {
private List propFiles = new ArrayList<>();
/**
* Evaluates chain files by name
* @param resourceName Absolute path of the resources containing chain information, e.g.
* config/properties.files
*
* If resourceFolders have been defined on the parent chain builder, resources will be looked up on the
* file system first and then on the classpath.
* @return This resolver
*/
public ChainFilesResolver name(String resourceName) {
List files = new ArrayList<>();
// Search resource folders
for (Path resourceFolder : resourceFolders) {
Path fsResource = resourceFolder.resolve(resourceName);
if (JuUrl.path().isExistingFile(fsResource)) {
files.add(JuUrl.toUrl(fsResource));
}
}
// Search classpath
files.addAll(JuUrl.resource().getAll(resourceName, false));
this.propFiles.addAll(files);
return this;
}
/**
* Resolves all evaluator using the chain file specified and returns the parent
* property chain builder.
* @return Parent property chain builder
*/
public PropertyChainBuilder resolve() {
logger.debug("Resolving PropertyChain by chain files");
// Process contents of prop files
XString duplicatePrios = new XString();
Map props = new TreeMap<>();
for (URL propFile : this.propFiles) {
logger.debug("Processing property file: " + propFile);
XString filteredContents = new XString("Filtered contents: " );
filteredContents.increaseIndent();
try (BufferedReader r = new IOUtil().createReader(propFile)) {
String line = r.readLine();
while (line != null) {
String lineParts[] = JuStringUtils.split(line, ",", true);
if (lineParts.length > 0 && !lineParts[0].startsWith("#")) {
AssertUtil.assertTrue("Invalid line: " + line, lineParts.length > 1);
// Process line
filteredContents.addLine(line);
int priorization = Integer.parseInt(lineParts[0]);
if (props.containsKey(priorization)) {
duplicatePrios.addLineFormatted("Duplicate priorization in %s: %d", propFile, priorization);
}
props.put(priorization, Arrays.copyOfRange(lineParts, 1, lineParts.length));
} else {
// Ignore line
}
line = r.readLine();
}
} catch (Exception ex) {
throw new JuRuntimeException("Couldn't process property file %s", ex, propFile);
}
logger.debug(filteredContents.toString());
if (!duplicatePrios.isEmpty()) {
throw new JuRuntimeException(duplicatePrios.toString());
}
}
// Build property chain from read info
PropertyChainBuilder chainBuilder = PropertyChainBuilder.this;
XString chainInfo = new XString("Evaluated property chain:");
chainInfo.increaseIndent();
for (int prio : props.keySet()) {
chainInfo.addLine(prio + ": ");
// Get the remaining line parts (without priorization)
String[] lineParts = props.get(prio);
String propType = lineParts[0];
// Perform placeholder substitution
for (int i = 1; i < lineParts.length; i++) {
String linePart = lineParts[i];
// Do interpolation
linePart = chainBuilder.peekChain().interpolate(linePart);
// Legacy Placeholder support...
XString part = new XString(linePart);
// Substitute %propertyKey% with the appropriate values...
for (String propertyKey : part.getPlaceHolders()) {
String val = chainBuilder.peekChain().get(propertyKey);
if (val != null) {
part.setPlaceholder(propertyKey, val);
} else {
logger.debug("Couldn't replace placeholder: " + propertyKey);
}
}
lineParts[i] = part.toString();
}
if ("sys".equals(propType)) {
chainBuilder.addSystemPropertyEvaluator();
chainInfo.addText("System Properties");
} else if ("prop".equals(propType)) {
AssertUtil.assertTrue("prop property type must be followed by a resource path", lineParts.length > 1);
String resourcePath = lineParts[1];
boolean optional = lineParts.length > 2 && "optional".equals(lineParts[2]);
URL resourceUrl = getFileResource(resourcePath, optional);
if (resourceUrl != null) {
chainBuilder.addResourcePropertyEvaluator(resourceUrl);
chainInfo.addText("Properties file: " + resourceUrl);
} else {
AssertUtil.assertTrue("Mandatory resource not found: " + resourcePath, optional);
chainInfo.addText("Properties file: >>> optional resource not found: " + resourcePath);
}
} else if ("csv".equals(propType)) {
AssertUtil.assertTrue(
"prop property type must be followed by a resource path and a profile property name", lineParts.length > 2);
String resourcePath = lineParts[1];
String profilePropertyName = lineParts[2];
String profileName = chainBuilder.getPropertyChain().get(profilePropertyName);
String defaultColumn = lineParts.length > 3
? lineParts[2]
: "default";
URL resourceUrl = getFileResource(resourcePath, false);
chainBuilder.addCsvPropertyEvaluator(resourceUrl, profileName, defaultColumn);
chainInfo.addFormatted("CSV Properties: %s [profileName=%s, defaultColumn=%s]"
, resourceUrl
, profileName
, defaultColumn);
} else {
throw new JuRuntimeException("Unsupported property type: " + propType);
}
}
// Output evaluated chain
logger.info(chainInfo.toString());
return PropertyChainBuilder.this;
}
}
private URL getFileResource(String resourceName, boolean optional) {
// Try lookup in resource folders
for (Path p : this.resourceFolders) {
Path resourcePath = p.resolve(resourceName);
if (JuUrl.path().isExistingFile(resourcePath)) {
return JuUrl.toUrl(resourcePath);
}
}
// Try lookup on classpath
return JuUrl.resource().single().exceptionIfNone(!optional).get(resourceName, false);
}
/**
* Helper class to configure property interpolation of a PropertyChain.
* @author [email protected]
*
*/
public final class InterpolationBuilder {
private boolean enabled = true;
private boolean envVariableInterpolation = true;
/**
* Sets whether property interpolation is enabled.
*
* This will enable property interpolation of type ${propName} as well as environmental variable
* interpolation of type ${env.ENV_NAME}.
* @param enableInterpolation If true, property interpolation is enabled. If false, it is disabled.
* @return Interpolation Builder
*/
public InterpolationBuilder enable(boolean enableInterpolation) {
this.enabled = enableInterpolation;
return this;
}
/**
* Finished interpolation configuration and returns the PropertyBuilder.
* @return PropertyChainBuilder to continue configuration
*/
public PropertyChainBuilder done() {
return PropertyChainBuilder.this;
}
}
private static class PropertyChainImpl implements InterpolatingPropertyChain {
private Logger logger = LoggerFactory.getLogger(PropertyChainImpl.class);
private final List evaluators;
private final JuTextEncryptor decryptor;
private final boolean defaultThrowExceptionIfUndefined;
private final Interpolator interpolator;
private final Set hiddenValueKeys;
private PropertyChainImpl(PropertyChainBuilder builder, boolean defaultThrowExceptionIfUndefined) {
this.defaultThrowExceptionIfUndefined = defaultThrowExceptionIfUndefined;
this.evaluators = new ArrayList<>(builder.evaluators);
this.decryptor = builder.decryptor;
this.hiddenValueKeys = new HashSet<>(builder.hiddenValueKeys);
if (builder.interpolationBuilder.enabled) {
this.interpolator = new RegexBasedInterpolator();
if (builder.interpolationBuilder.envVariableInterpolation) {
try {
this.interpolator.addValueSource(new EnvarBasedValueSource());
} catch (Exception ex) {
throw new JuRuntimeException("Couldn't create EnvarBasedValueSource", ex);
}
}
this.interpolator.addValueSource(new ChainValueSource());
} else {
this.interpolator = null;
}
}
@Override
public String get(String key) {
return this.get(key, this.defaultThrowExceptionIfUndefined);
}
@Override
public String get(String key, boolean throwExceptionIfNotDefined) {
Object obj = this.getObject(key, null, throwExceptionIfNotDefined);
return obj == null ? null : obj.toString();
}
@Override
public String get(String key, String defaultValue) {
String val = this.get(key, false);
return val != null ? val : defaultValue;
}
@Override
public T get(String key, Class clazz) {
String val = this.get(key);
return this.convert(val, clazz);
}
@Override
public T get(String key, Class clazz, boolean throwExceptionIfNotDefined) {
String val = this.get(key, throwExceptionIfNotDefined);
return this.convert(val, clazz);
}
@Override
public T get(String key, Class clazz, String defaultValue) {
String val = this.get(key, defaultValue);
return this.convert(val, clazz);
}
@SuppressWarnings("unchecked")
private T convert(String val, Class clazz) {
if (StringUtils.isEmpty(val)) return null;
if (clazz == Integer.class) {
return (T) new Integer(val);
} else if (clazz == Boolean.class) {
return (T) new Boolean(val);
} else if (clazz == Object.class) {
return (T) val;
} else {
throw new JuRuntimeException("Conversion not supported: " + clazz);
}
}
private Object getObject(String key, Object defaultValue, boolean throwExceptionIfNotDefined) {
PropertyInfoImpl pi = this.evaluteAndInterpolate(key);
if (pi == null || pi.getValue() == null) {
if (throwExceptionIfNotDefined) {
throw new JuRuntimeException("Property undefined: " + key);
} else {
return defaultValue;
}
}
return pi.getValue();
}
private PropertyInfoImpl evaluteAndInterpolate(String key) {
PropertyInfoImpl pi = this.evaluate(key);
if (this.interpolator != null && pi != null && pi.rawValue instanceof String) {
try{
String interpolatedValue = this.interpolator.interpolate(pi.getValue());
pi.setValue(interpolatedValue);
return pi;
} catch (InterpolationException ex) {
logger.warn("Couldn't interpolate " + pi.getValue(), ex);
return pi;
}
} else {
return pi;
}
}
private PropertyInfoImpl evaluate(String key) {
for (PropertyEvaluator evaluator : evaluators) {
Object val = evaluator.get(key);
if (val != null) {
String stringVal = val.toString();
PropertyInfoImpl pi = new PropertyInfoImpl(key, val, evaluator.toString());
// Check if we should hide value
if (this.hiddenValueKeys.contains(key)) {
pi.setDisplayValue(ENCRYPTED_VALUE_LOGGING_STRING);
pi.setSensitive(true);
}
// Check if the value is encrypted
if (JuSecurityUtils.isEncryptedByTag(stringVal)) {
if (decryptor != null) {
pi.setValue(JuSecurityUtils.decryptTaggedValueIfNecessary(stringVal, decryptor));
pi.setDisplayValue(ENCRYPTED_VALUE_LOGGING_STRING);
pi.setSensitive(true);
} else {
logger.warn("Value seems to be encrypted, but no decrypted was set on the PropertyChain");
}
}
logger.debug("Evaluated property: {}", pi);
return pi;
}
}
return null;
}
@Override
public Set listKeys() {
Set keys = new LinkedHashSet<>();
for (PropertyEvaluator evaluator : evaluators) {
keys.addAll(evaluator.listKeys());
}
return keys;
}
@Override
public PropertyInfo getInfo(String key) {
return this.evaluteAndInterpolate(key);
}
private class ChainValueSource extends AbstractValueSource {
public ChainValueSource() {
super(false);
}
@Override
public Object getValue(String expression) {
PropertyInfoImpl pi = PropertyChainImpl.this.evaluate(expression);
return pi != null ? pi.getValue() : null;
}
}
@Override
public String interpolate(String expression) {
if (this.interpolator != null) {
try {
return this.interpolator.interpolate(expression);
} catch (Exception ex) {
logger.warn("Couldn't interpolate expression: " + expression, ex);
}
}
return expression;
}
}
private static class SystemPropertyEvaluator implements PropertyEvaluator {
@Override
public Object get(String key) {
return System.getProperty(key);
};
@Override
public String toString() {
return JuStringUtils.toString(this);
}
@Override
public Set listKeys() {
return JuCollectionUtils.getKeyStrings(System.getProperties());
}
}
private static class PropertiesPropertyEvaluator implements PropertyEvaluator {
private final URL propertiesUrl;
private final Properties props;
public PropertiesPropertyEvaluator(Properties props) {
this.propertiesUrl = null;
this.props = props;
}
public PropertiesPropertyEvaluator(URL propertiesUrl) throws JuException {
this.propertiesUrl = propertiesUrl;
this.props = new IOUtil().loadPropertiesFromUrl(propertiesUrl);
}
@Override
public Object get(String key) {
return this.props == null ? null : this.props.get(key);
};
@Override
public String toString() {
if (this.propertiesUrl != null) {
return JuStringUtils.toString(this, "url", this.propertiesUrl);
} else {
return JuStringUtils.toString(this);
}
}
@Override
public Set listKeys() {
return JuCollectionUtils.getKeyStrings(props);
}
}
private static class CsvPropertyEvaluator implements PropertyEvaluator {
private final URL resourceUrl;
private final String profile;
private final CsvTableLookup csvTable;
public CsvPropertyEvaluator(URL resourceUrl, String profile, String defaultColumn) {
this.resourceUrl = resourceUrl;
this.profile = profile;
this.csvTable = CsvTableLookup.build()
.from(resourceUrl)
.defaultColumn(defaultColumn)
.create();
}
@Override
public Object get(String key) {
return this.csvTable.get(key, this.profile);
}
@Override
public String toString() {
return JuStringUtils.toString(this
, "url", this.resourceUrl
, "profile", this.profile);
}
@Override
public Set listKeys() {
Set keys = new LinkedHashSet();
keys.addAll(this.csvTable.getKeys());
return keys;
}
}
private static class PropertyChainPropertyEvaluator implements PropertyEvaluator {
private final PropertyChain nestedChain;
public PropertyChainPropertyEvaluator(PropertyChain chain) {
this.nestedChain = chain;
}
@Override
public Object get(String key) {
return this.nestedChain.get(key, Object.class);
}
@Override
public Set listKeys() {
return this.nestedChain.listKeys();
}
@Override
public String toString() {
return JuStringUtils.toString(this
, "nestedChain", this.nestedChain);
}
}
private static class PropertyInfoImpl implements PropertyInfo {
private final String key;
private final Object rawValue;
private final String evaluatorInfo;
private Object value;
private String displayValue;
private boolean isSensitive;
private PropertyInfoImpl(String key, Object rawValue, String evaluatorInfo) {
this.key = key;
this.rawValue = rawValue;
this.evaluatorInfo = evaluatorInfo;
this.value = rawValue;
this.displayValue = rawValue == null ? null : rawValue.toString();
}
public void setDisplayValue(String displayValue) {
this.displayValue = displayValue;
}
public void setSensitive(boolean isSensitive) {
this.isSensitive = isSensitive;
}
@Override
public String getKey() {
return this.key;
}
private void setValue(Object val) {
if (ObjectUtils.equals(this.value, this.displayValue)) {
this.displayValue = (val == null ? null : val.toString());
}
this.value = val;
}
@Override
public String getValue() {
return this.value == null ? null : this.value.toString();
}
@Override
public String getDisplayValue() {
return this.displayValue == null ? null : this.displayValue;
}
@Override
public String getRawValue() {
return this.rawValue == null ? null : this.rawValue.toString();
}
@Override
public boolean isSensitive() {
return this.isSensitive;
}
@Override
public String getEvaluatorInfo() {
return this.evaluatorInfo;
}
@Override
public String toString() {
return String.format("%s=%s [using %s]", this.getKey(), this.getDisplayValue(), this.getEvaluatorInfo());
}
}
}