Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom string substitutors functionality #108

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions owner/src/main/java/org/aeonbits/owner/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@
package org.aeonbits.owner;


import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.aeonbits.owner.Config.HotReloadType.SYNC;
import static org.aeonbits.owner.Config.LoadType.FIRST;
import static org.aeonbits.owner.Util.ignore;
import static org.aeonbits.owner.Util.reverse;

import java.io.IOException;
import java.io.Serializable;
import java.lang.annotation.Documented;
Expand All @@ -19,15 +28,6 @@
import java.util.Properties;
import java.util.concurrent.TimeUnit;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.aeonbits.owner.Config.HotReloadType.SYNC;
import static org.aeonbits.owner.Config.LoadType.FIRST;
import static org.aeonbits.owner.Util.ignore;
import static org.aeonbits.owner.Util.reverse;

/**
* Marker interface that must be implemented by all Config sub-interfaces.
* <p>
Expand Down Expand Up @@ -329,4 +329,16 @@ enum DisableableFeature {
Class<? extends Converter> value();
}

/**
* Registers {@link Substitutor} classes, with given names, to allow the user to define custom
* substitutor logic when expanding variables in property strings.
*/
@Retention(RUNTIME)
@Target(TYPE)
@Documented
@interface SubstitutorClasses {
String[] names();
Class<? extends Substitutor>[] classes();
}

}
6 changes: 3 additions & 3 deletions owner/src/main/java/org/aeonbits/owner/DefaultFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@

package org.aeonbits.owner;

import org.aeonbits.owner.loaders.Loader;
import static java.lang.reflect.Proxy.newProxyInstance;

import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ScheduledExecutorService;

import static java.lang.reflect.Proxy.newProxyInstance;
import org.aeonbits.owner.loaders.Loader;

/**
* Default implementation for {@link Factory}.
Expand All @@ -39,7 +39,7 @@ public <T extends Config> T create(Class<? extends T> clazz, Map<?, ?>... import
VariablesExpander expander = new VariablesExpander(props);
PropertiesManager manager = new PropertiesManager(clazz, new Properties(), scheduler, expander, loadersManager,
imports);
PropertiesInvocationHandler handler = new PropertiesInvocationHandler(manager);
PropertiesInvocationHandler handler = new PropertiesInvocationHandler(manager, clazz);
T proxy = (T) newProxyInstance(clazz.getClassLoader(), interfaces, handler);
handler.setProxy(proxy);
return proxy;
Expand Down
24 changes: 24 additions & 0 deletions owner/src/main/java/org/aeonbits/owner/DefaultSubstitutor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.aeonbits.owner;

import java.io.Serializable;
import java.util.Properties;


final class DefaultSubstitutor implements Substitutor, Serializable {

private final Properties variables;

DefaultSubstitutor(Properties props) {
variables = props;
}

public String replace(String strToReplace) {
return variables.getProperty(strToReplace);
}

@Override
public String toString() {
return variables.toString();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@

package org.aeonbits.owner;

import static org.aeonbits.owner.Config.DisableableFeature.PARAMETER_FORMATTING;
import static org.aeonbits.owner.Config.DisableableFeature.VARIABLE_EXPANSION;
import static org.aeonbits.owner.Converters.convert;
import static org.aeonbits.owner.PropertiesMapper.key;
import static org.aeonbits.owner.Util.isFeatureDisabled;
import static org.aeonbits.owner.util.Reflection.invokeDefaultMethod;
import static org.aeonbits.owner.util.Reflection.isDefault;

import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
Expand All @@ -16,14 +24,7 @@
import java.util.LinkedList;
import java.util.List;

import static org.aeonbits.owner.Config.DisableableFeature.PARAMETER_FORMATTING;
import static org.aeonbits.owner.Config.DisableableFeature.VARIABLE_EXPANSION;
import static org.aeonbits.owner.Converters.convert;
import static org.aeonbits.owner.PropertiesManager.Delegate;
import static org.aeonbits.owner.PropertiesMapper.key;
import static org.aeonbits.owner.Util.isFeatureDisabled;
import static org.aeonbits.owner.util.Reflection.invokeDefaultMethod;
import static org.aeonbits.owner.util.Reflection.isDefault;
import org.aeonbits.owner.PropertiesManager.Delegate;

/**
* This {@link InvocationHandler} receives method calls from the delegate instantiated by {@link ConfigFactory} and maps
Expand All @@ -44,9 +45,9 @@ class PropertiesInvocationHandler implements InvocationHandler, Serializable {
private final StrSubstitutor substitutor;
final PropertiesManager propertiesManager;

PropertiesInvocationHandler(PropertiesManager manager) {
PropertiesInvocationHandler(PropertiesManager manager, Class<? extends Config> clazz) {
this.propertiesManager = manager;
this.substitutor = new StrSubstitutor(manager.load());
this.substitutor = new StrSubstitutor(manager.load(), clazz);
}

public Object invoke(Object proxy, Method invokedMethod, Object... args) throws Throwable {
Expand Down
69 changes: 63 additions & 6 deletions owner/src/main/java/org/aeonbits/owner/StrSubstitutor.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@

package org.aeonbits.owner;

import static java.util.regex.Pattern.compile;
import static org.aeonbits.owner.Util.fixBackslashForRegex;
import static org.aeonbits.owner.Util.unsupported;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.util.regex.Pattern.compile;
import static org.aeonbits.owner.Util.fixBackslashForRegex;
import org.aeonbits.owner.Config.SubstitutorClasses;

/**
* <p>
Expand Down Expand Up @@ -48,8 +53,10 @@
*/
class StrSubstitutor implements Serializable {

private final Properties values;
private static final Pattern PATTERN = compile("\\$\\{(.+?)\\}");
private static final String DEFAULT_SUBSTITUTOR_NAME = "";
private static final int TO_SIZE_2 = 2;
private final Map<String, Substitutor> substitutors = new HashMap<String, Substitutor>();

/**
* Creates a new instance and initializes it. Uses defaults for variable prefix and suffix and the escaping
Expand All @@ -58,7 +65,41 @@ class StrSubstitutor implements Serializable {
* @param values the variables' values, may be null
*/
StrSubstitutor(Properties values) {
this.values = values;
substitutors.put(DEFAULT_SUBSTITUTOR_NAME, new DefaultSubstitutor(values));
}

StrSubstitutor(Properties values, Class<? extends Config> clazz) {
substitutors.put(DEFAULT_SUBSTITUTOR_NAME, new DefaultSubstitutor(values));
addCustomSubstitutors(clazz);
}

private void addCustomSubstitutors(Class<? extends Config> configClazz) {
SubstitutorClasses annotation = configClazz.getAnnotation(SubstitutorClasses.class);
if (annotation != null) {
checkSameSize(annotation);
for (int i = 0; i < annotation.names().length; i++) {
String name = annotation.names()[i];
Class<? extends Substitutor> clazz = annotation.classes()[i];
try {
substitutors.put(name, clazz.newInstance());
} catch (Exception e) {
throw unsupported(e,
"Substitutor class '%s' cannot be instantiated; see the cause below in the stack trace",
clazz.getCanonicalName());
}
}
}

}

private void checkSameSize(SubstitutorClasses annotation) {
int numOfNames = annotation.names().length;
int numOfClasses = annotation.classes().length;
if (numOfNames != numOfClasses) {
throw unsupported(
"Mismatch in number of names (%s) and classes (%s) in annotation %s",
numOfNames, numOfClasses, annotation);
}
}

/**
Expand All @@ -74,13 +115,29 @@ String replace(String source) {
Matcher m = PATTERN.matcher(source);
StringBuffer sb = new StringBuffer();
while (m.find()) {
String var = m.group(1);
String value = values.getProperty(var);
String value = replaceValueUsingSubstitutors(m.group(1));
String replacement = (value != null) ? replace(value) : "";
m.appendReplacement(sb, fixBackslashForRegex(replacement));
}
m.appendTail(sb);
return sb.toString();
}

private String replaceValueUsingSubstitutors(final String strToReplace) {
String name = DEFAULT_SUBSTITUTOR_NAME;
String value = strToReplace;
if (strToReplace.contains(":")) {
String[] pair = strToReplace.split(":", TO_SIZE_2);
name = pair[0];
value = pair[1];
}
if (name.equals(DEFAULT_SUBSTITUTOR_NAME)) {
return substitutors.get(DEFAULT_SUBSTITUTOR_NAME).replace(strToReplace);
}
if (substitutors.containsKey(name)) {
return substitutors.get(name).replace(value);
}
return substitutors.get(DEFAULT_SUBSTITUTOR_NAME).replace(strToReplace);
}

}
21 changes: 21 additions & 0 deletions owner/src/main/java/org/aeonbits/owner/Substitutor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.aeonbits.owner;

import org.aeonbits.owner.Config.SubstitutorClasses;

/**
* Substitutor interface specifies how to provide a custom substitutor mechanism when replacing
* variables in strings. Substitutors are registered by name and implementation class using
* {@link SubstitutorClasses} annotation. For example this string "${name:some-value}" the
* "some-value" part will be sent to the Substitutor implementation registered
* with the name "name".
*/
public interface Substitutor {

/**
* Replaces the given {@code strToReplace} to something custom.
*
* @param strToReplace the value to replace
* @return the replacement value
*/
String replace(String strToReplace);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@

package org.aeonbits.owner;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.verify;

import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.Properties;
import java.util.concurrent.ScheduledExecutorService;

import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.verify;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;

/**
* @author Luigi R. Viggiano
Expand All @@ -47,7 +47,7 @@ interface Dummy extends Config {}
@Before
public void before() {
PropertiesManager loader = new PropertiesManager(Dummy.class, properties, scheduler, expander, loaders);
handler = new PropertiesInvocationHandler(loader);
handler = new PropertiesInvocationHandler(loader, Dummy.class);
}

@Test
Expand Down
Loading