diff --git a/core/src/main/java/foundation/stack/datamill/configuration/BooleanValue.java b/core/src/main/java/foundation/stack/datamill/configuration/BooleanValue.java new file mode 100644 index 0000000..4dcf129 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/BooleanValue.java @@ -0,0 +1,20 @@ +package foundation.stack.datamill.configuration; + +import java.lang.annotation.*; + +/** + * Annotation to be used together with a {@link foundation.stack.datamill.configuration.impl.ConstantsClassSource} to + * specify String values. For example, a constant declared in a constants class as: + * + * \@BooleanValue(true) public static final String PROPERTY_NAME = "configuration/propertyName"; + * + * This defines a property called "configuration/propertyName" that has the boolean value true. + * + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +public @interface BooleanValue { + boolean value(); +} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/IntegerValue.java b/core/src/main/java/foundation/stack/datamill/configuration/IntegerValue.java new file mode 100644 index 0000000..0080320 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/IntegerValue.java @@ -0,0 +1,20 @@ +package foundation.stack.datamill.configuration; + +import java.lang.annotation.*; + +/** + * Annotation to be used together with a {@link foundation.stack.datamill.configuration.impl.ConstantsClassSource} to + * specify String values. For example, a constant declared in a constants class as: + * + * \@IntegerValue(16) public static final String PROPERTY_NAME = "configuration/propertyName"; + * + * This defines a property called "configuration/propertyName" that has the integer value 16. + * + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +public @interface IntegerValue { + int value(); +} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/Named.java b/core/src/main/java/foundation/stack/datamill/configuration/Named.java index 213a780..4a3b081 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/Named.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/Named.java @@ -10,7 +10,7 @@ */ @Documented @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.PARAMETER) +@Target({ElementType.PARAMETER}) public @interface Named { String value(); } diff --git a/core/src/main/java/foundation/stack/datamill/configuration/Properties.java b/core/src/main/java/foundation/stack/datamill/configuration/Properties.java index e7f07e6..323901a 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/Properties.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/Properties.java @@ -12,6 +12,11 @@ * @author Ravi Chodavarapu (rchodava@gmail.com) */ public class Properties { + /** @see PropertySourceChain#orConstantsClass(Class) */ + public static PropertySourceChain fromConstantsClass(Class constantsClass) { + return fromSource(new ConstantsClassSource<>(constantsClass)); + } + /** @see PropertySourceChain#orFile(String) */ public static PropertySourceChain fromFile(String path) { try { diff --git a/core/src/main/java/foundation/stack/datamill/configuration/PropertySourceChain.java b/core/src/main/java/foundation/stack/datamill/configuration/PropertySourceChain.java index 3ffc770..82cf525 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/PropertySourceChain.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/PropertySourceChain.java @@ -30,6 +30,14 @@ public interface PropertySourceChain extends PropertySource { */ PropertySourceChain alias(String alias, String original); + /** + * Add a property source to the chain which retrieves properties from a constants interface or class. The constants + * are expected to + * + * @param constantsClass Constants class to add as a source. + */ + PropertySourceChain orConstantsClass(Class constantsClass); + /** * Add a property source to the chain which retrieves properties specified in the file at the specified path. * diff --git a/core/src/main/java/foundation/stack/datamill/configuration/StringValue.java b/core/src/main/java/foundation/stack/datamill/configuration/StringValue.java new file mode 100644 index 0000000..26c4b7d --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/StringValue.java @@ -0,0 +1,20 @@ +package foundation.stack.datamill.configuration; + +import java.lang.annotation.*; + +/** + * Annotation to be used together with a {@link foundation.stack.datamill.configuration.impl.ConstantsClassSource} to + * specify String values. For example, a constant declared in a constants class as: + * + * \@StringValue("value") public static final String PROPERTY_NAME = "configuration/propertyName"; + * + * This defines a property called "configuration/propertyName" that has the value "value". + * + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +public @interface StringValue { + String value(); +} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/Wiring.java b/core/src/main/java/foundation/stack/datamill/configuration/Wiring.java index 947fc73..ca88057 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/Wiring.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/Wiring.java @@ -426,7 +426,27 @@ private T constructWithConstructor( return null; } } else { - logger.error("Could not build class {} as the following type was not found {}", clazz, parameterType); + StringBuilder parameterNames = new StringBuilder(); + + Named[] annotations = parameters[i].getAnnotationsByType(Named.class); + if (annotations != null && annotations.length > 0) { + for (Named annotation : annotations) { + if (parameterNames.length() > 0) { + parameterNames.append(", "); + } + + parameterNames.append(annotation.value()); + } + } + + if (parameterNames.length() > 0) { + logger.error("Could not build class {} as the following named parameter was not found: {}", + clazz, parameterNames.toString()); + } else { + logger.error("Could not build class {} as the following type was not found {}", + clazz, parameterType); + } + return null; } } diff --git a/core/src/main/java/foundation/stack/datamill/configuration/impl/ConstantsClassSource.java b/core/src/main/java/foundation/stack/datamill/configuration/impl/ConstantsClassSource.java new file mode 100644 index 0000000..018ed69 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/ConstantsClassSource.java @@ -0,0 +1,85 @@ +package foundation.stack.datamill.configuration.impl; + +import foundation.stack.datamill.configuration.BooleanValue; +import foundation.stack.datamill.configuration.IntegerValue; +import foundation.stack.datamill.configuration.StringValue; +import javassist.Modifier; + +import java.lang.reflect.Field; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public class ConstantsClassSource extends AbstractSource { + private static String getConstantValue(Field field) { + if (field != null && Modifier.isStatic(field.getModifiers())) { + try { + performSecure(() -> field.setAccessible(true)); + return String.valueOf(field.get(null)); + } catch (IllegalAccessException e) { + return null; + } + } + + return null; + } + + private static void performSecure(Runnable runnable) { + if (System.getSecurityManager() != null) { + AccessController.doPrivileged((PrivilegedAction) () -> { + runnable.run(); + return null; + }); + } else { + runnable.run(); + } + } + + private final Class constantsClass; + private Map annotatedConstants; + + public ConstantsClassSource(Class constantsClass) { + this.constantsClass = constantsClass; + } + + private Map buildAnnotatedConstants() { + HashMap constants = new HashMap<>(); + + Field[] fields = constantsClass.getDeclaredFields(); + for (Field field : fields) { + String propertyName = getConstantValue(field); + if (propertyName != null) { + StringValue stringValue = field.getAnnotation(StringValue.class); + if (stringValue != null) { + constants.put(propertyName, stringValue.value()); + } else { + BooleanValue booleanValue = field.getAnnotation(BooleanValue.class); + if (booleanValue != null) { + constants.put(propertyName, String.valueOf(booleanValue.value())); + } else { + IntegerValue integerValue = field.getAnnotation(IntegerValue.class); + if (integerValue != null) { + constants.put(propertyName, String.valueOf(integerValue.value())); + } + } + } + } + } + + return constants; + } + + @Override + protected Optional getOptional(String name) { + if (annotatedConstants == null) { + annotatedConstants = buildAnnotatedConstants(); + } + + return Optional.ofNullable(annotatedConstants.get(name)); + } +} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/impl/PropertySourceChainImpl.java b/core/src/main/java/foundation/stack/datamill/configuration/impl/PropertySourceChainImpl.java index 1d8242d..fdf98ee 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/impl/PropertySourceChainImpl.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/PropertySourceChainImpl.java @@ -43,6 +43,11 @@ public Optional getOptional(String name) { return Optional.empty(); } + @Override + public PropertySourceChain orConstantsClass(Class constantsClass) { + return orSource(new ConstantsClassSource<>(constantsClass)); + } + @Override public PropertySource orDefaults(Action1 defaultsInitializer) { DefaultsSource defaults = new DefaultsSource(); diff --git a/core/src/test/java/foundation/stack/datamill/configuration/PropertiesTest.java b/core/src/test/java/foundation/stack/datamill/configuration/PropertiesTest.java index d054ca5..c7b7819 100644 --- a/core/src/test/java/foundation/stack/datamill/configuration/PropertiesTest.java +++ b/core/src/test/java/foundation/stack/datamill/configuration/PropertiesTest.java @@ -42,6 +42,26 @@ public void systemProperties() { assertEquals("value", Properties.fromSystem().getRequired("test").asString()); } + @Test + public void constantClasses() { + assertEquals("value", Properties.fromConstantsClass(ConstantsClass.class).get(ConstantsClass.property).get()); + assertFalse(Properties.fromConstantsClass(ConstantsClass.class).get("property2").isPresent()); + assertFalse(Properties.fromConstantsClass(ConstantsClass.class).get("config/instance").isPresent()); + + assertEquals("publicValue", Properties.fromConstantsClass(ConstantsClass.class).get(ConstantsClass.publicProperty).get()); + assertEquals("privateValue", Properties.fromConstantsClass(ConstantsClass.class).get(ConstantsClass.privateProperty).get()); + assertEquals("nonFinalValue", Properties.fromConstantsClass(ConstantsClass.class).get(ConstantsClass.nonFinalProperty).get()); + assertEquals("1", Properties.fromConstantsClass(ConstantsClass.class).get(ConstantsClass.integerProperty).get()); + assertEquals("true", Properties.fromConstantsClass(ConstantsClass.class).get(ConstantsClass.booleanProperty).get()); + + assertEquals("value", Properties.fromConstantsClass(ConstantsClass.class).getRequired("config/property").asString()); + + assertEquals("ifacePublic", Properties.fromConstantsClass(ConstantsClass.class).orConstantsClass(ConstantsInterface.class).get(ConstantsInterface.IFACE_PUBLIC_TEST).get()); + assertEquals("2", Properties.fromConstantsClass(ConstantsClass.class).orConstantsClass(ConstantsInterface.class).get(ConstantsInterface.IFACE_INTEGER).get()); + assertEquals("true", Properties.fromConstantsClass(ConstantsClass.class).orConstantsClass(ConstantsInterface.class).get(ConstantsInterface.IFACE_BOOLEAN).get()); + assertEquals("true", Properties.fromConstantsClass(ConstantsClass.class).orConstantsClass(ConstantsInterface.class).get(ConstantsClass.booleanProperty).get()); + } + @Test public void files() { assertEquals("value", Properties.fromFile("test.properties").get("test").get()); @@ -131,4 +151,20 @@ public void missingRequiredProperties() throws Exception { } catch (IllegalArgumentException e) { } } + + private static class ConstantsClass { + @StringValue("value") static final String property = "config/property"; + @StringValue("publicValue") public static final String publicProperty = "config/public"; + @StringValue("privateValue") private static final String privateProperty = "config/private"; + @StringValue("nonFinalValue") static String nonFinalProperty = "config/nonFinal"; + @IntegerValue(1) static String integerProperty = "config/integer"; + @BooleanValue(true) static String booleanProperty = "config/boolean"; + @StringValue("instanceValue") String instanceProperty = "config/instance"; + } + + private interface ConstantsInterface { + @StringValue("ifacePublic") String IFACE_PUBLIC_TEST = "iface/public"; + @IntegerValue(2) String IFACE_INTEGER = "iface/integer"; + @BooleanValue(true) String IFACE_BOOLEAN = "iface/boolean"; + } }