LUCENE-8261: InterpolatedProperties.interpolate and recursive property references.

This commit is contained in:
Dawid Weiss 2018-05-07 13:22:11 +02:00
parent f9942b525b
commit 445c0aa47e
2 changed files with 114 additions and 16 deletions

View File

@ -183,6 +183,9 @@ Bug Fixes
Other
* LUCENE-8261: InterpolatedProperties.interpolate and recursive property
references. (Steve Rowe, Dawid Weiss)
* LUCENE-8228: removed obsolete IndexDeletionPolicy clone() requirements from
the javadoc. (Dawid Weiss)

View File

@ -19,17 +19,28 @@ package org.apache.lucene.dependencies;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Parse a properties file, performing non-recursive Ant-like
* property value interpolation, and return the resulting Properties.
*/
public class InterpolatedProperties extends Properties {
private static final Pattern PROPERTY_REFERENCE_PATTERN = Pattern.compile("\\$\\{([^}]+)\\}");
private static final Pattern PROPERTY_REFERENCE_PATTERN = Pattern.compile("\\$\\{(?<name>[^}]+)\\}");
/**
* Loads the properties file via {@link Properties#load(InputStream)},
@ -46,26 +57,110 @@ public class InterpolatedProperties extends Properties {
*/
@Override
public void load(Reader reader) throws IOException {
super.load(reader);
interpolate();
Properties p = new Properties();
p.load(reader);
LinkedHashMap<String, String> props = new LinkedHashMap<>();
Enumeration<?> e = p.propertyNames();
while (e.hasMoreElements()) {
String key = (String) e.nextElement();
props.put(key, p.getProperty(key));
}
resolve(props).forEach((k, v) -> this.setProperty(k, v));
}
/**
* Perform non-recursive Ant-like property value interpolation
*/
private void interpolate() {
StringBuffer buffer = new StringBuffer();
for (Map.Entry<?,?> entry : entrySet()) {
buffer.setLength(0);
Matcher matcher = PROPERTY_REFERENCE_PATTERN.matcher(entry.getValue().toString());
private static Map<String,String> resolve(Map<String,String> props) {
LinkedHashMap<String, String> resolved = new LinkedHashMap<>();
HashSet<String> recursive = new HashSet<>();
props.forEach((k, v) -> {
resolve(props, resolved, recursive, k, v);
});
return resolved;
}
private static String resolve(Map<String,String> props,
LinkedHashMap<String, String> resolved,
Set<String> recursive,
String key,
String value) {
if (value == null) {
throw new IllegalArgumentException("Missing replaced property key: " + key);
}
if (recursive.contains(key)) {
throw new IllegalArgumentException("Circular recursive property resolution: " + recursive);
}
if (!resolved.containsKey(key)) {
recursive.add(key);
StringBuffer buffer = new StringBuffer();
Matcher matcher = PROPERTY_REFERENCE_PATTERN.matcher(value);
while (matcher.find()) {
String interpolatedValue = getProperty(matcher.group(1));
if (null != interpolatedValue) {
matcher.appendReplacement(buffer, interpolatedValue);
}
String referenced = matcher.group("name");
String concrete = resolve(props, resolved, recursive, referenced, props.get(referenced));
matcher.appendReplacement(buffer, Matcher.quoteReplacement(concrete));
}
matcher.appendTail(buffer);
setProperty((String) entry.getKey(), buffer.toString());
resolved.put(key, buffer.toString());
recursive.remove(key);
}
assert resolved.get(key).equals(value);
return resolved.get(key);
}
public static void main(String [] args) {
{
Map<String, String> props = new LinkedHashMap<>();
props.put("a", "${b}");
props.put("b", "${c}");
props.put("c", "foo");
props.put("d", "${a}/${b}/${c}");
assertEquals(resolve(props), "a=foo", "b=foo", "c=foo", "d=foo/foo/foo");
}
{
Map<String, String> props = new LinkedHashMap<>();
props.put("a", "foo");
props.put("b", "${a}");
assertEquals(resolve(props), "a=foo", "b=foo");
}
{
Map<String, String> props = new LinkedHashMap<>();
props.put("a", "${b}");
props.put("b", "${c}");
props.put("c", "${a}");
try {
resolve(props);
} catch (IllegalArgumentException e) {
// Expected, circular reference.
if (!e.getMessage().contains("Circular recursive")) {
throw new AssertionError();
}
}
}
{
Map<String, String> props = new LinkedHashMap<>();
props.put("a", "${b}");
try {
resolve(props);
} catch (IllegalArgumentException e) {
// Expected, no referenced value.
if (!e.getMessage().contains("Missing replaced")) {
throw new AssertionError();
}
}
}
}
private static void assertEquals(Map<String,String> resolved, String... keyValuePairs) {
List<String> result = resolved.entrySet().stream().sorted((a, b) -> a.getKey().compareTo(b.getKey()))
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.toList());
if (!result.equals(Arrays.asList(keyValuePairs))) {
throw new AssertionError("Mismatch: \n" + result + "\nExpected: " + Arrays.asList(keyValuePairs));
}
}
}