hm.binkley.util.XProperties Maven / Gradle / Ivy
/*
* This is free and unencumbered software released into the public domain.
*
* Please see https://github.com/binkley/binkley/blob/master/LICENSE.md.
*/
package hm.binkley.util;
import hm.binkley.util.Converter.Conversion;
import lombok.NonNull;
import org.apache.commons.lang3.text.StrLookup;
import org.apache.commons.lang3.text.StrSubstitutor;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.BufferedReader;
import java.io.CharArrayReader;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.lang.String.format;
import static java.lang.Thread.currentThread;
import static java.util.Arrays.asList;
import static java.util.regex.Pattern.compile;
/**
* {@code XProperties} is a {@code java.util.Properties} supporting inclusion of other properties
* files, parameter substition and typed return values. Key order is preserved; keys from included
* properties are prior to those from the current properties.
*
* Keys are restricted to {@code String}s.
*
* Loading processes lines of the form:
* #include Spring style resource path
for inclusion. Looks up resource
* path with a {@link ResourcePatternResolver}; loading found resources in turn. Similarly
* processes resource recursively for further inclusion. Includes multiple resources separated by
* comma (",") in the same "#include" statement.
*
* Substitutes in resource paths of the
* form:
* #include ${variable}
or portions thereof. Looks up variable in
* the current properties, and if not found, in the system properties. If found, replaces the text
* with the variable value, including "${" and trailing "}".
*
* Use forward slashes ("/") only for path separators; do not use back slashes
* ("\").
*
* Examples:
Text Result {@code #include
* foo/file.properties} Includes {@code foo.properties} found in the classpath
* {@code #include foo.properties, bar.properties} Includes both {@code
* foo.properties} and {@code bar.properties} {@code #include
* file:/var/tmp/${user.name }/foo.properties} Includes {@code foo.properties} found in a
* directory named after the current user {@code #include
* classpath*:**/foo.properties} Includes all {@code foo.properties} files found
* subdirectories of the classpath
*
* @todo Implement defaults
* @todo Converter assumes cacheable keys; is this correct?
* @see PathMatchingResourcePatternResolver
* @see StrSubstitutor
* @see Converter
* @see #load(Reader) loading properties with inclusions
* @see #load(InputStream) loading properties with inclusions
* @see #getProperty(String) getting properties with substitution
*/
public class XProperties
extends OrderedProperties {
private static final Pattern include = compile("^#include\\s+(.*)\\s*$");
private static final Pattern comma = compile("\\s*,\\s*");
private static final Pattern colon = compile(":");
private final Set included = new LinkedHashSet<>();
private final Converter converter = new Converter();
private final Map converted = new ConcurrentHashMap<>();
private final StrSubstitutor substitutor = new StrSubstitutor(new FindValue());
{
substitutor.setEnableSubstitutionInVariables(true);
}
/**
* Creates a new {@code XProperties} for the given absolutePath found in the
* classpath.
*
* @param absolutePath the absolute path to search on the classpath, never missing
*
* @throws IOException if absolutePath cannot be loaded
*/
@Nonnull
public static XProperties from(@Nonnull @NonNull final String absolutePath)
throws IOException {
final URL resource = XProperties.class.getResource(absolutePath);
try (final InputStream in = resource.openStream()) {
final XProperties xprops = new XProperties();
xprops.included.add(URI.create(resource.toString()));
xprops.load(in);
return xprops;
}
}
/** @todo Documentation */
public XProperties() {
}
/** @todo Documentation */
public XProperties(@Nonnull final Map initial) {
putAll(initial);
}
/**
* @param prefix the alias prefix, never missing
* @param factory the factory, never missing
*
* @todo Documentation
* @see Converter#register(String, Conversion)
*/
public void register(@Nonnull final String prefix,
@Nonnull final Conversion, ? extends Exception> factory) {
converter.register(prefix, factory);
}
/**
* Note {@code XProperties} description for additional features over plain properties loading.
* {@inheritDoc}
*
* @throws IOException if the properties cannot be loaded or if included resources cannot be
* read
*/
@Override
public synchronized void load(@Nonnull final Reader reader)
throws IOException {
final ResourcePatternResolver loader = new PathMatchingResourcePatternResolver(
currentThread().getContextClassLoader());
try (final CharArrayWriter writer = new CharArrayWriter()) {
try (final BufferedReader lines = new BufferedReader(reader)) {
for (String line = lines.readLine(); null != line; line = lines.readLine()) {
writer.append(line).append('\n');
final Matcher matcher = include.matcher(line);
if (matcher.matches())
for (final String x : comma.split(substitutor.replace(matcher.group(1))))
for (final Resource resource : loader.getResources(x)) {
final URI uri = resource.getURI();
if (!included.add(uri))
throw new RecursiveIncludeException(uri, included);
try (final InputStream in = resource.getInputStream()) {
load(in);
}
}
}
}
super.load(new CharArrayReader(writer.toCharArray()));
}
included.clear();
}
/**
* Note {@code XProperties} description for additional features over plain properties loading.
* {@inheritDoc}
*
* @throws IOException if the properties cannot be loaded or if included resources cannot be
* read
*/
@Override
public synchronized void load(final InputStream inStream)
throws IOException {
load(new InputStreamReader(inStream, Charset.forName("UTF-8")));
}
/**
* {@inheritDoc}
* Substitutes in property values sequences of the form:
* ${variable}
. Looks up variable in the current properties, and
* if not found, in the system properties. If found, replaces the text with the variable value,
* including "${" and trailing "}".
*
* @throws MissingPropertyException if substitution refers to a missing property
* @see StrSubstitutor
*/
@Nullable
@Override
public String getProperty(final String key) {
final String value = super.getProperty(key);
if (null == value)
return null;
return substitutor.replace(value);
}
/**
* {@inheritDoc}
* Substitutes in string values sequences of the form:
* ${variable}
for substition. Looks up variable in the current
* properties, and if not found, in the system properties. If found, replaces the text with the
* variable value, including "${" and trailing "}".
*
* @see StrSubstitutor
*/
@Nullable
@Override
public synchronized Object get(final Object key) {
final Object value = super.get(key);
if (null == value || !(value instanceof String))
return value;
return substitutor.replace((String) value);
}
/**
* Gets a typed property value. Responsibility is the caller's to assign the
* return to a correct type; failure to do so will cause a {@code ClassCastException} at
* run-time.
*
* Typed keys are of the form: {@code type:key}. The key is
* the same key as {@link System#getProperty(String) System.getProperty}. See {@link
* Converter#register(String, Conversion) register} for built-in type key prefixes.
*
* Examples:
Code Comment {@code Integer foo =
* xprops.getObject("int:foo");} Gets the "foo" property as an possibly {@code null}
* integer {@code int foo = xprops.getObject("int:foo");} Gets the
* "foo" property as a primitive integer, throwing {@code NullPointerException} if
* missing {@code Long foo = xprops.getObject("long:foo");} Gets the
* "foo" property as an possibly {@code null} long; this is the same property as the previous
* examples
*
* @param key the type-prefixed key, never missing
* @param the value type
*
* @return the type property value, possibly missing
*
* @see #getObjectOrDefault(String, Object)
* @see Converter#register(String, Conversion)
*/
@Nullable
public T getObject(@Nonnull final String key) {
return getObjectOrDefault(key, null);
}
/** @todo Javadoc */
@Nullable
@SuppressWarnings("unchecked")
public T getObjectOrDefault(@Nonnull final String key, final T defaultValue) {
return (T) converted.computeIfAbsent(new Key(key, defaultValue), this::convert);
}
private Object convert(final Key key) {
final String property = key.property;
final String wholeValue = getProperty(property);
if (null != wholeValue)
return wholeValue; // Literal key match wins - assume T==String
final String[] parts = colon.split(property, 2);
final String value = getProperty(parts[1]);
if (null == value)
return key.fallback;
try {
return converter.convert(parts[0], value);
} catch (final Exception e) {
final String[] x = colon.split(property, 2);
throw new FailedConversionException(property, getProperty(x[1]), e.getCause());
}
}
private final class FindValue
extends StrLookup {
@Override
public String lookup(final String key) {
return asList((Function) XProperties.this::getProperty,
System::getProperty, System::getenv).stream().
map(f -> f.apply(key)).
filter(v -> null != v).
findFirst().
orElseThrow(() -> new MissingPropertyException(key));
}
}
/** @todo Documentation */
public static final class RecursiveIncludeException
extends RuntimeException {
public RecursiveIncludeException(final URI duplicate, final Collection included) {
super(message(duplicate, included));
}
private static String message(final URI duplicate, final Collection included) {
final ArrayList x = new ArrayList<>(included.size() + 1);
x.addAll(included);
x.add(duplicate);
return x.toString();
}
}
/** @todo Documentation */
public static final class FailedConversionException
extends RuntimeException {
public FailedConversionException(final String key, final String value,
final Throwable cause) {
super(format("%s = %s", key, value), cause);
}
}
/** @todo Documentation */
public static class MissingPropertyException
extends IllegalArgumentException {
public MissingPropertyException(final String key) {
super(key);
}
}
private static final class Key {
private final String property;
private final Object fallback;
private Key(final String property, final Object fallback) {
this.property = property;
this.fallback = fallback;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy