From d995c1323dd0abf7574ba5504145e3b62e7371b3 Mon Sep 17 00:00:00 2001 From: Ravi Chodavarapu Date: Thu, 13 Apr 2017 19:13:46 +0100 Subject: [PATCH 1/5] Allow using a class with constants as a property source Also, improve the error when a wiring involving a named property fails --- .../datamill/configuration/BooleanValue.java | 20 +++++ .../datamill/configuration/IntegerValue.java | 20 +++++ .../stack/datamill/configuration/Named.java | 2 +- .../datamill/configuration/Properties.java | 5 ++ .../configuration/PropertySourceChain.java | 8 ++ .../datamill/configuration/StringValue.java | 20 +++++ .../stack/datamill/configuration/Wiring.java | 22 ++++- .../impl/ConstantsClassSource.java | 85 +++++++++++++++++++ .../impl/PropertySourceChainImpl.java | 5 ++ .../configuration/PropertiesTest.java | 36 ++++++++ 10 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/BooleanValue.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/IntegerValue.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/StringValue.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/impl/ConstantsClassSource.java 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"; + } } From 1c63ae4aaf2ad52e5df788abf73096a0e8739f61 Mon Sep 17 00:00:00 2001 From: Ravi Chodavarapu Date: Fri, 14 Apr 2017 12:08:05 +0100 Subject: [PATCH 2/5] Allow a callback to be used to prepare a connection before migrating --- .../stack/datamill/db/DatabaseClient.java | 102 +++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/foundation/stack/datamill/db/DatabaseClient.java b/core/src/main/java/foundation/stack/datamill/db/DatabaseClient.java index 9ca8687..1a9de62 100644 --- a/core/src/main/java/foundation/stack/datamill/db/DatabaseClient.java +++ b/core/src/main/java/foundation/stack/datamill/db/DatabaseClient.java @@ -6,9 +6,12 @@ import foundation.stack.datamill.db.impl.RowImpl; import foundation.stack.datamill.db.impl.UnsubscribeOnNextOperator; import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationInfo; +import org.flywaydb.core.api.callback.FlywayCallback; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import rx.Observable; +import rx.functions.Action1; import rx.functions.Func1; import javax.sql.DataSource; @@ -111,8 +114,17 @@ public void clean() { getFlyway().clean(); } + public void migrate(Action1 migrationPreparation) { + Flyway flyway = getFlyway(); + if (migrationPreparation != null) { + flyway.setCallbacks(new MigrationCallback(migrationPreparation)); + } + + flyway.migrate(); + } + public void migrate() { - getFlyway().migrate(); + migrate(null); } @Override @@ -212,4 +224,92 @@ public Observable stream() { return results; } } + + private static class MigrationCallback implements FlywayCallback { + private final Action1 migrationAction; + + public MigrationCallback(Action1 migrationAction) { + this.migrationAction = migrationAction; + } + + @Override + public void beforeClean(Connection connection) { + + } + + @Override + public void afterClean(Connection connection) { + + } + + @Override + public void beforeMigrate(Connection connection) { + migrationAction.call(connection); + } + + @Override + public void afterMigrate(Connection connection) { + + } + + @Override + public void beforeEachMigrate(Connection connection, MigrationInfo info) { + + } + + @Override + public void afterEachMigrate(Connection connection, MigrationInfo info) { + + } + + @Override + public void beforeValidate(Connection connection) { + + } + + @Override + public void afterValidate(Connection connection) { + + } + + @Override + public void beforeBaseline(Connection connection) { + + } + + @Override + public void afterBaseline(Connection connection) { + + } + + @Override + public void beforeInit(Connection connection) { + + } + + @Override + public void afterInit(Connection connection) { + + } + + @Override + public void beforeRepair(Connection connection) { + + } + + @Override + public void afterRepair(Connection connection) { + + } + + @Override + public void beforeInfo(Connection connection) { + + } + + @Override + public void afterInfo(Connection connection) { + + } + } } From f5e5aca273c7c9a3c743d8751ba92b7246de9dbb Mon Sep 17 00:00:00 2001 From: Ravi Chodavarapu Date: Sat, 15 Apr 2017 02:26:29 +0100 Subject: [PATCH 3/5] Have lambda ApiHandler implement AWS Lambda interface for request handlers Improve the construction of property source chains - allow transformers for any property source, add computed and delegating property sources, change defaults to an immediate properties source --- .../datamill/configuration/Defaults.java | 11 - .../DelegatingPropertySource.java | 29 ++ .../ImmediatePropertySource.java | 11 + .../datamill/configuration/Properties.java | 53 --- .../configuration/PropertySource.java | 11 +- .../configuration/PropertySourceChain.java | 315 +++++++++++++++--- .../PropertySourceChainContinuation.java | 99 ++++++ .../stack/datamill/configuration/Wiring.java | 36 -- .../configuration/impl/AbstractSource.java | 20 +- .../configuration/impl/ComputedSource.java | 21 ++ .../configuration/impl/DefaultsSource.java | 25 -- .../impl/EnvironmentPropertiesSource.java | 14 +- .../impl/ImmediatePropertySourceImpl.java | 25 ++ .../impl/PropertySourceChainImpl.java | 95 ------ .../impl/SystemPropertiesSource.java | 14 +- .../configuration/PropertiesTest.java | 170 ---------- .../PropertySourceChainTest.java | 237 +++++++++++++ .../datamill/configuration/WiringTest.java | 2 +- .../cucumber/PlaceholderResolver.java | 4 +- .../foundation/stack/lambda/ApiHandler.java | 6 +- .../stack/lambda/ApiHandlerTest.java | 12 +- 21 files changed, 714 insertions(+), 496 deletions(-) delete mode 100644 core/src/main/java/foundation/stack/datamill/configuration/Defaults.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/DelegatingPropertySource.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/ImmediatePropertySource.java delete mode 100644 core/src/main/java/foundation/stack/datamill/configuration/Properties.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/PropertySourceChainContinuation.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/impl/ComputedSource.java delete mode 100644 core/src/main/java/foundation/stack/datamill/configuration/impl/DefaultsSource.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/impl/ImmediatePropertySourceImpl.java delete mode 100644 core/src/main/java/foundation/stack/datamill/configuration/impl/PropertySourceChainImpl.java delete mode 100644 core/src/test/java/foundation/stack/datamill/configuration/PropertiesTest.java create mode 100644 core/src/test/java/foundation/stack/datamill/configuration/PropertySourceChainTest.java diff --git a/core/src/main/java/foundation/stack/datamill/configuration/Defaults.java b/core/src/main/java/foundation/stack/datamill/configuration/Defaults.java deleted file mode 100644 index e860ec6..0000000 --- a/core/src/main/java/foundation/stack/datamill/configuration/Defaults.java +++ /dev/null @@ -1,11 +0,0 @@ -package foundation.stack.datamill.configuration; - -/** - * Used in defining defaults for properties. - * - * @see PropertySourceChain - * @author Ravi Chodavarapu (rchodava@gmail.com) - */ -public interface Defaults { - Defaults put(String name, String value); -} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/DelegatingPropertySource.java b/core/src/main/java/foundation/stack/datamill/configuration/DelegatingPropertySource.java new file mode 100644 index 0000000..f609c4c --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/DelegatingPropertySource.java @@ -0,0 +1,29 @@ +package foundation.stack.datamill.configuration; + +import foundation.stack.datamill.configuration.impl.AbstractSource; + +import java.util.Optional; + +/** + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public class DelegatingPropertySource extends AbstractSource { + private volatile PropertySource delegate; + + public DelegatingPropertySource(PropertySource delegate) { + setDelegate(delegate); + } + + public DelegatingPropertySource() { + this(null); + } + + public void setDelegate(PropertySource delegate) { + this.delegate = delegate; + } + + @Override + protected Optional getOptional(String name) { + return delegate != null ? delegate.get(name) : Optional.empty(); + } +} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/ImmediatePropertySource.java b/core/src/main/java/foundation/stack/datamill/configuration/ImmediatePropertySource.java new file mode 100644 index 0000000..6542e55 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/ImmediatePropertySource.java @@ -0,0 +1,11 @@ +package foundation.stack.datamill.configuration; + +/** + * Used in defining an immediate immutable set of properties. + * + * @see PropertySourceChain + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public interface ImmediatePropertySource { + ImmediatePropertySource put(String name, 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 deleted file mode 100644 index 323901a..0000000 --- a/core/src/main/java/foundation/stack/datamill/configuration/Properties.java +++ /dev/null @@ -1,53 +0,0 @@ -package foundation.stack.datamill.configuration; - -import foundation.stack.datamill.configuration.impl.*; -import rx.functions.Func1; - -import java.io.IOException; - -/** - * Starting points for creating {@link PropertySourceChain}s. - * - * @see PropertySourceChain - * @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 { - return fromSource(new FileSource(path)); - } catch (IOException e) { - return fromSource(EmptySource.INSTANCE); - } - } - - /** @see PropertySourceChain#orSource(PropertySource) */ - public static PropertySourceChain fromSource(PropertySource source) { - return new PropertySourceChainImpl(source); - } - - /** @see PropertySourceChain#orEnvironment() */ - public static PropertySourceChain fromEnvironment() { - return fromSource(EnvironmentPropertiesSource.IDENTITY); - } - - /** @see PropertySourceChain#orEnvironment(Func1) */ - public static PropertySourceChain fromEnvironment(Func1 transformer) { - return fromSource(new EnvironmentPropertiesSource(transformer)); - } - - /** @see PropertySourceChain#orSystem() */ - public static PropertySourceChain fromSystem() { - return fromSource(SystemPropertiesSource.IDENTITY); - } - - /** @see PropertySourceChain#orSystem(Func1) */ - public static PropertySourceChain fromSystem(Func1 transformer) { - return fromSource(new SystemPropertiesSource(transformer)); - } -} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/PropertySource.java b/core/src/main/java/foundation/stack/datamill/configuration/PropertySource.java index ea7fe0e..8b72d5c 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/PropertySource.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/PropertySource.java @@ -6,20 +6,11 @@ import java.util.Optional; /** - * A source of properties. + * A source of properties. Use {@link PropertySourceChain} to create {@link PropertySource}s. * * @author Ravi Chodavarapu (rchodava@gmail.com) */ public interface PropertySource { - /** - * Alias a property so that the alias can be used in a {@link #get(String)} call in order to retrieve the original - * property's value. - * - * @param alias New alias for the original property. - * @param original Original property to create an alias for. - */ - PropertySource alias(String alias, String original); - /** * Get the specified property from the source, if it exists. * 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 82cf525..0a66d87 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/PropertySourceChain.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/PropertySourceChain.java @@ -1,89 +1,320 @@ package foundation.stack.datamill.configuration; +import com.google.common.base.CaseFormat; +import foundation.stack.datamill.configuration.impl.*; import rx.functions.Action1; import rx.functions.Func1; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + /** - * Allows {@link PropertySource}s to be linked together into a chain so that alternative sources of desired properties + *

+ * Allows {@link PropertySource}s to be composed together into a chain so that alternative sources of desired properties * can be specified when looking up configuration properties. For example, consider a chain composed and used as follows: *

*

- * chain = Properties.fromFile("service.properties").orFile("defaults.properties").orEnvironment()
- *     .orDefaults(defaults -> defaults.put("property1", "value1").put("property2", "value2"));
+ * chain = PropertySourceChain.ofFile("service.properties").orFile("defaults.properties").orEnvironment()
+ *     .orImmediate(defaults -> defaults.put("property1", "value1").put("property2", "value2"));
  *
  * chain.getRequired("property1");
  * chain.getRequired("property3");
  * 
- *

+ *

* This chain will first attempt to retrieve requested properties from the service.properties file. It will consult the * defaults.properties file next if it can't find properties in service.properties. Subsequently, it will attempt to * look for those properties amongst environment variables if it still fails to find a requested property. Finally, - * it will return the defaults specified by the orDefaults(...) call. + * it will return the defaults specified by the orImmediate(...) call. *

- * Note that each of the chaining methods returns a new chain with the additional property source added. + *

+ * Note that each of the methods accepts a transformer to transform the names of the properties before looking up + * the property in that particular property source. See {@link Transformers} for some useful common transformers. + *

+ *

+ * Note that each of the chaining methods returns a new chain with the additional property source added. This allows you + * to have common chains which you can combine in different ways, because each of those combinations is a new chain. For + * example: + *

+ *
+ * chain1 = COMMON.orEnvironment(property -> CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, property));
+ * chain2 = COMMON.orFile("defaults.properties");
+ * 
+ *

+ * Each of these chains is a separate chain that has some common section. + *

* * @author Ravi Chodavarapu (rchodava@gmail.com) */ -public interface PropertySourceChain extends PropertySource { +public final class PropertySourceChain extends AbstractSource implements PropertySourceChainContinuation { /** - * @see PropertySource#alias(String, String) + * @see PropertySourceChainContinuation#orConstantsClass(Class) */ - PropertySourceChain alias(String alias, String original); + public static PropertySourceChainContinuation ofConstantsClass(Class constantsClass) { + return ofSource(new ConstantsClassSource<>(constantsClass)); + } /** - * 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. + * @see PropertySourceChainContinuation#orConstantsClass(Class, Func1) */ - PropertySourceChain orConstantsClass(Class constantsClass); + public static PropertySourceChainContinuation ofConstantsClass(Class constantsClass, Func1 transformer) { + return ofSource(new ConstantsClassSource<>(constantsClass), transformer); + } /** - * Add a property source to the chain which retrieves properties specified in the file at the specified path. - * - * @param path Path to properties file to add as a source. + * @see PropertySourceChainContinuation#orComputed(Func1) */ - PropertySourceChain orFile(String path); + public static PropertySourceChainContinuation ofComputed(Func1 computation) { + return ofComputed(computation, null); + } /** - * Add a source which looks up properties defined as environment variables to the chain. + * @see PropertySourceChainContinuation#orComputed(Func1, Func1) */ - PropertySourceChain orEnvironment(); + public static PropertySourceChainContinuation ofComputed(Func1 computation, Func1 transformer) { + return ofSource(new ComputedSource(computation), transformer); + } + + public static PropertySourceChainContinuation ofFile(String path) { + return ofFile(path, null); + } /** - * Add a source which looks up properties defined as environment variables but first transforms the property names - * using the specified function before looking up the environment variable. - * - * @param transformer Transformation function used to transform property names before looking them up as - * environment variables. For example, you may want to prefix property names before looking them - * up as environment variables. + * @see PropertySourceChainContinuation#orFile(String) */ - PropertySourceChain orEnvironment(Func1 transformer); + public static PropertySourceChainContinuation ofFile(String path, Func1 transformer) { + try { + return ofSource(new FileSource(path), transformer); + } catch (IOException e) { + return ofSource(EmptySource.INSTANCE); + } + } /** - * Add a custom property source to the chain at this point.. - * - * @param source Source to add to the chain. + * @see PropertySourceChainContinuation#orImmediate(Action1) */ - PropertySourceChain orSource(PropertySource source); + public static PropertySourceChainContinuation ofImmediate(Action1 initializer) { + return ofImmediate(initializer, null); + } /** - * Add a source which looks up properties defined as system properties to the chain. + * @see PropertySourceChainContinuation#orImmediate(Action1, Func1) */ - PropertySourceChain orSystem(); + public static PropertySourceChainContinuation ofImmediate(Action1 initializer, Func1 transformer) { + ImmediatePropertySourceImpl immediateSource = new ImmediatePropertySourceImpl(); + initializer.call(immediateSource); + + return ofSource(immediateSource, transformer); + } /** - * Add a source which looks up properties defined as system properties but first transforms the property names - * using the specified function before looking up the system property. - * - * @see #orEnvironment(Func1) + * @see PropertySourceChainContinuation#orSource(PropertySource) */ - PropertySourceChain orSystem(Func1 transformer); + public static PropertySourceChainContinuation ofSource(PropertySource source) { + return new PropertySourceChain(source, null); + } + + /** + * @see PropertySourceChainContinuation#orSource(PropertySource, Func1) + */ + public static PropertySourceChainContinuation ofSource(PropertySource source, Func1 transformer) { + return new PropertySourceChain(source, transformer); + } + + /** + * @see PropertySourceChainContinuation#orEnvironment() + */ + public static PropertySourceChainContinuation ofEnvironment() { + return ofEnvironment(null); + } + + /** + * @see PropertySourceChainContinuation#orEnvironment(Func1) + */ + public static PropertySourceChainContinuation ofEnvironment(Func1 transformer) { + return ofSource(EnvironmentPropertiesSource.DEFAULT, transformer); + } + + /** + * @see PropertySourceChainContinuation#orSystem() + */ + public static PropertySourceChainContinuation ofSystem() { + return ofSystem(null); + } + + /** + * @see PropertySourceChainContinuation#orSystem(Func1) + */ + public static PropertySourceChainContinuation ofSystem(Func1 transformer) { + return ofSource(SystemPropertiesSource.DEFAULT, transformer); + } + + private final List chain; + + private PropertySourceChain(PropertySource initialSource, Func1 transformer) { + chain = Collections.singletonList(new TransformedSource(initialSource, transformer)); + } + + private PropertySourceChain(List chain) { + this.chain = chain; + } + + @Override + public Optional getOptional(String name) { + for (TransformedSource source : chain) { + Optional value = source.propertySource.get(source.transformer.call(name)); + if (value.isPresent()) { + return value; + } + } + + return Optional.empty(); + } + + @Override + public PropertySourceChain orComputed(Func1 computation) { + return orSource(new ComputedSource(computation), null); + } + + @Override + public PropertySourceChain orComputed(Func1 computation, Func1 transformer) { + return orSource(new ComputedSource(computation), transformer); + } + + @Override + public PropertySourceChain orConstantsClass(Class constantsClass) { + return orConstantsClass(constantsClass, null); + } + + @Override + public PropertySourceChain orConstantsClass(Class constantsClass, Func1 transformer) { + return orSource(new ConstantsClassSource<>(constantsClass), transformer); + } + + @Override + public PropertySourceChain orImmediate(Action1 initializer) { + return orImmediate(initializer, null); + } + + @Override + public PropertySourceChain orImmediate(Action1 initializer, Func1 transformer) { + ImmediatePropertySourceImpl defaults = new ImmediatePropertySourceImpl(); + initializer.call(defaults); + + return orSource(defaults, transformer); + } + + @Override + public PropertySourceChain orFile(String path) { + return orFile(path, null); + } + + @Override + public PropertySourceChain orFile(String path, Func1 transformer) { + try { + return orSource(new FileSource(path), transformer); + } catch (IOException e) { + return orSource(EmptySource.INSTANCE); + } + } + + @Override + public PropertySourceChain orEnvironment() { + return orEnvironment(null); + } + + @Override + public PropertySourceChain orEnvironment(Func1 transformer) { + return orSource(EnvironmentPropertiesSource.DEFAULT, transformer); + } + + @Override + public PropertySourceChain orSource(PropertySource source) { + return orSource(source, null); + } + + @Override + public PropertySourceChain orSource(PropertySource source, Func1 transformer) { + ArrayList newChain = new ArrayList<>(chain); + newChain.add(new TransformedSource(source, transformer)); + + return new PropertySourceChain(newChain); + } + + @Override + public PropertySourceChain orSystem() { + return orSystem(null); + } + + @Override + public PropertySourceChain orSystem(Func1 transformer) { + return orSource(SystemPropertiesSource.DEFAULT, transformer); + } + + private static class TransformedSource { + private final PropertySource propertySource; + private final Func1 transformer; + + public TransformedSource(PropertySource propertySource, Func1 transformer) { + this.propertySource = propertySource; + this.transformer = transformer != null ? transformer : Transformers.IDENTITY; + } + } /** - * Add defaults for properties that cannot be found by any other source in the chain. - * - * @param defaultsInitializer Function which sets up defaults for properties not found in the chain. + * Some common transformers that are useful when creating {@link PropertySourceChain}. */ - PropertySource orDefaults(Action1 defaultsInitializer); + public static class Transformers { + private static final Func1 IDENTITY = name -> name; + + /** + * Transforms from lower-camel case to upper-underscore case, for example: propertyName -> PROPERTY_NAME. + */ + public static final Func1 LOWER_CAMEL_TO_UPPER_UNDERSCORE = + name -> CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, name); + + /** + * Same as calling {@link #leaf(char)} with '/'. + */ + public static final Func1 LEAF = leaf('/'); + + /** + *

+ * Returns a transformation function that applies the first function, and then the second function on the result + * of the first. So compose(LEAF, LOWER_CAMEL_TO_UPPER_UNDERSCORE) will return a function that transforms: + *

+ *

+ * category/propertyName -> PROPERTY_NAME + *

+ */ + public static final Func1 compose(Func1 a, Func1 b) { + return name -> b.call(a.call(name)); + } + + /** + *

+ * Returns a transformation function that returns the leaf name of a name separated by the specified separator + * character. For example: + *

+ *

+ * leaf('/') returns a function which transforms: category/propertyName -> propertyName + * leaf(':') returns a function which transforms: category:propertyName -> propertyName + *

+ */ + public static final Func1 leaf(char separator) { + return name -> { + if (name != null) { + int leafSeparator = name.lastIndexOf('/'); + if (leafSeparator > 0) { + return name.substring(leafSeparator + 1); + } + } + + return name; + }; + } + } + } diff --git a/core/src/main/java/foundation/stack/datamill/configuration/PropertySourceChainContinuation.java b/core/src/main/java/foundation/stack/datamill/configuration/PropertySourceChainContinuation.java new file mode 100644 index 0000000..f271855 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/PropertySourceChainContinuation.java @@ -0,0 +1,99 @@ +package foundation.stack.datamill.configuration; + +import rx.functions.Action1; +import rx.functions.Func1; + +/** + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public interface PropertySourceChainContinuation extends PropertySource { + /** + * Add a property source to the chain which computes the value of property using the specified function. The + * function can return either a value, or a null to indicate it cannot compute the value of the property - in this + * case, the chain will look to the next source. + * + * @param computation Computation function for the source. + */ + PropertySourceChain orComputed(Func1 computation); + + /** + * @See PropertySourceChain + * @see #orComputed(Func1) + */ + PropertySourceChain orComputed(Func1 computation, Func1 transformer); + + /** + * Add a property source to the chain which retrieves properties from a constants interface or class. The interface + * or class is expected to define String constants that are annotated with {@link StringValue} annotations. + * + * @param constantsClass Constants class to add as a source. + */ + PropertySourceChain orConstantsClass(Class constantsClass); + + /** + * @See PropertySourceChain + * @see #orConstantsClass(Class) + */ + PropertySourceChain orConstantsClass(Class constantsClass, Func1 transformer); + + /** + * Add immediate set of properties that can be looked up at this point in the chain. + * + * @param initializer Function which sets up immediate properties at this point in the chain. + */ + PropertySourceChain orImmediate(Action1 initializer); + + /** + * @See PropertySourceChain + * @see #orImmediate(Action1) + */ + PropertySourceChain orImmediate(Action1 initializer, Func1 transformer); + + /** + * Add a property source to the chain which retrieves properties specified in the file at the specified path. + * + * @param path Path to properties file to add as a source. + */ + PropertySourceChain orFile(String path); + + /** + * @See PropertySourceChain + * @see #orFile(String) + */ + PropertySourceChain orFile(String path, Func1 transformer); + + /** + * Add a source which looks up properties defined as environment variables to the chain. + */ + PropertySourceChain orEnvironment(); + + /** + * @See PropertySourceChain + * @see #orEnvironment() + */ + PropertySourceChain orEnvironment(Func1 transformer); + + /** + * Add a custom property source to the chain at this point. + * + * @param source Source to add to the chain. + */ + PropertySourceChain orSource(PropertySource source); + + /** + * @See PropertySourceChain + * @see #orSource(PropertySource) + */ + PropertySourceChain orSource(PropertySource source, Func1 transformer); + + /** + * Add a source which looks up properties defined as system properties to the chain. + */ + PropertySourceChain orSystem(); + + /** + * @See PropertySourceChain + * @see #orSystem() + */ + PropertySourceChain orSystem(Func1 transformer); +} 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 ca88057..8d32da3 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/Wiring.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/Wiring.java @@ -356,24 +356,6 @@ private T construct(Class clazz, Set> autoConstructionCandidates throw new IllegalStateException("Unable to satisfy all dependencies needed to construct instance of " + clazz.getName()); } - /** - * Check if the wiring already contains an instance of the provided class. If it exists, return it, otherwise, delegate - * to {@link #construct(Class)} in order to construct it. - * - * @param clazz Class we want to create an instance of. - * @param Type of instance. - * @return Instance that was constructed. - * @throws IllegalArgumentException If the class is an interface, abstract class or has no public constructors. - * @throws IllegalStateException If all dependencies for constructing an instance cannot be satisfied. - */ - public T constructIfMissing(Class clazz) { - T target = get(clazz); - if (target != null) { - return target; - } - return construct(clazz); - } - /** * Construct an instance of the specified class using a public constructors that has parameters of the specified * types. @@ -389,24 +371,6 @@ public T constructWith(Class clazz, Class... parameterTypes) { } } - /** - * Check if the wiring already contains an instance of the provided class. If it exists, return it, otherwise, delegate - * to {@link #constructWith(Class, Class[])} in order to construct it. - * - * @param clazz Class we want to create an instance of. - * @param parameterTypes Parameter types to be matched in constructor to be used. - * @return Instance that was constructed. - * @throws IllegalArgumentException If the class is an interface, abstract class or has no public constructors. - * @throws IllegalStateException If all dependencies for constructing an instance cannot be satisfied. - */ - public T constructWithIfMissing(Class clazz, Class... parameterTypes) { - T target = get(clazz); - if (target != null) { - return target; - } - return constructWith(clazz, parameterTypes); - } - private T constructWithConstructor( Class clazz, Constructor constructor, diff --git a/core/src/main/java/foundation/stack/datamill/configuration/impl/AbstractSource.java b/core/src/main/java/foundation/stack/datamill/configuration/impl/AbstractSource.java index 49b2ced..2a049a5 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/impl/AbstractSource.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/AbstractSource.java @@ -5,35 +5,17 @@ import foundation.stack.datamill.values.Value; import rx.functions.Action1; -import java.util.HashMap; -import java.util.Map; import java.util.Optional; /** * @author Ravi Chodavarapu (rchodava@gmail.com) */ public abstract class AbstractSource implements PropertySource { - private final Map aliases = new HashMap<>(); - - @Override - public PropertySource alias(String alias, String original) { - aliases.put(alias, original); - return this; - } - protected abstract Optional getOptional(String name); @Override public final Optional get(String name) { - Optional value = getOptional(name); - if (!value.isPresent()) { - String original = aliases.get(name); - if (original != null) { - value = getOptional(original); - } - } - - return value; + return getOptional(name); } @Override diff --git a/core/src/main/java/foundation/stack/datamill/configuration/impl/ComputedSource.java b/core/src/main/java/foundation/stack/datamill/configuration/impl/ComputedSource.java new file mode 100644 index 0000000..599f9e8 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/ComputedSource.java @@ -0,0 +1,21 @@ +package foundation.stack.datamill.configuration.impl; + +import rx.functions.Func1; + +import java.util.Optional; + +/** + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public class ComputedSource extends AbstractSource { + private final Func1 computation; + + public ComputedSource(Func1 computation) { + this.computation = computation; + } + + @Override + public Optional getOptional(String name) { + return Optional.ofNullable(computation.call(name)); + } +} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/impl/DefaultsSource.java b/core/src/main/java/foundation/stack/datamill/configuration/impl/DefaultsSource.java deleted file mode 100644 index 8dd11ab..0000000 --- a/core/src/main/java/foundation/stack/datamill/configuration/impl/DefaultsSource.java +++ /dev/null @@ -1,25 +0,0 @@ -package foundation.stack.datamill.configuration.impl; - -import foundation.stack.datamill.configuration.Defaults; - -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -/** - * @author Ravi Chodavarapu (rchodava@gmail.com) - */ -public class DefaultsSource extends AbstractSource implements Defaults { - private final Map defaults = new HashMap<>(); - - @Override - public Optional getOptional(String name) { - return Optional.ofNullable(defaults.get(name)); - } - - @Override - public Defaults put(String name, String value) { - defaults.put(name, value); - return this; - } -} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/impl/EnvironmentPropertiesSource.java b/core/src/main/java/foundation/stack/datamill/configuration/impl/EnvironmentPropertiesSource.java index ce5d102..318a33d 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/impl/EnvironmentPropertiesSource.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/EnvironmentPropertiesSource.java @@ -1,28 +1,18 @@ package foundation.stack.datamill.configuration.impl; -import com.github.davidmoten.rx.Functions; -import rx.functions.Func1; - import java.util.Optional; /** * @author Ravi Chodavarapu (rchodava@gmail.com) */ public class EnvironmentPropertiesSource extends AbstractSource { - public static final EnvironmentPropertiesSource IDENTITY = new EnvironmentPropertiesSource(); - - private final Func1 transformer; - - public EnvironmentPropertiesSource(Func1 transformer) { - this.transformer = transformer != null ? transformer : Functions.identity(); - } + public static final EnvironmentPropertiesSource DEFAULT = new EnvironmentPropertiesSource(); private EnvironmentPropertiesSource() { - this(null); } @Override public Optional getOptional(String name) { - return Optional.ofNullable(System.getenv(transformer.call(name))); + return Optional.ofNullable(System.getenv(name)); } } diff --git a/core/src/main/java/foundation/stack/datamill/configuration/impl/ImmediatePropertySourceImpl.java b/core/src/main/java/foundation/stack/datamill/configuration/impl/ImmediatePropertySourceImpl.java new file mode 100644 index 0000000..a65df50 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/ImmediatePropertySourceImpl.java @@ -0,0 +1,25 @@ +package foundation.stack.datamill.configuration.impl; + +import foundation.stack.datamill.configuration.ImmediatePropertySource; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public class ImmediatePropertySourceImpl extends AbstractSource implements ImmediatePropertySource { + private final Map immediates = new HashMap<>(); + + @Override + public Optional getOptional(String name) { + return Optional.ofNullable(immediates.get(name)); + } + + @Override + public ImmediatePropertySource put(String name, String value) { + immediates.put(name, value); + return this; + } +} 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 deleted file mode 100644 index fdf98ee..0000000 --- a/core/src/main/java/foundation/stack/datamill/configuration/impl/PropertySourceChainImpl.java +++ /dev/null @@ -1,95 +0,0 @@ -package foundation.stack.datamill.configuration.impl; - -import foundation.stack.datamill.configuration.Defaults; -import foundation.stack.datamill.configuration.PropertySource; -import foundation.stack.datamill.configuration.PropertySourceChain; -import rx.functions.Action1; -import rx.functions.Func1; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -/** - * @author Ravi Chodavarapu (rchodava@gmail.com) - */ -public class PropertySourceChainImpl extends AbstractSource implements PropertySourceChain { - private final List chain; - - public PropertySourceChainImpl(PropertySource initialSource) { - chain = Collections.singletonList(initialSource); - } - - private PropertySourceChainImpl(List chain) { - this.chain = chain; - } - - @Override - public PropertySourceChain alias(String alias, String original) { - return (PropertySourceChain) super.alias(alias, original); - } - - @Override - public Optional getOptional(String name) { - for (PropertySource source : chain) { - Optional value = source.get(name); - if (value.isPresent()) { - return value; - } - } - - return Optional.empty(); - } - - @Override - public PropertySourceChain orConstantsClass(Class constantsClass) { - return orSource(new ConstantsClassSource<>(constantsClass)); - } - - @Override - public PropertySource orDefaults(Action1 defaultsInitializer) { - DefaultsSource defaults = new DefaultsSource(); - defaultsInitializer.call(defaults); - - return orSource(defaults); - } - - @Override - public PropertySourceChain orEnvironment() { - return orSource(EnvironmentPropertiesSource.IDENTITY); - } - - @Override - public PropertySourceChain orEnvironment(Func1 transformer) { - return orSource(new EnvironmentPropertiesSource(transformer)); - } - - @Override - public PropertySourceChain orFile(String path) { - try { - return orSource(new FileSource(path)); - } catch (IOException e) { - return orSource(EmptySource.INSTANCE); - } - } - - @Override - public PropertySourceChain orSource(PropertySource source) { - ArrayList newChain = new ArrayList<>(chain); - newChain.add(source); - - return new PropertySourceChainImpl(newChain); - } - - @Override - public PropertySourceChain orSystem() { - return orSource(SystemPropertiesSource.IDENTITY); - } - - @Override - public PropertySourceChain orSystem(Func1 transformer) { - return orSource(new SystemPropertiesSource(transformer)); - } -} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/impl/SystemPropertiesSource.java b/core/src/main/java/foundation/stack/datamill/configuration/impl/SystemPropertiesSource.java index 1330759..7f77706 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/impl/SystemPropertiesSource.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/SystemPropertiesSource.java @@ -1,28 +1,18 @@ package foundation.stack.datamill.configuration.impl; -import com.github.davidmoten.rx.Functions; -import rx.functions.Func1; - import java.util.Optional; /** * @author Ravi Chodavarapu (rchodava@gmail.com) */ public class SystemPropertiesSource extends AbstractSource { - public static final SystemPropertiesSource IDENTITY = new SystemPropertiesSource(); - - private final Func1 transformer; - - public SystemPropertiesSource(Func1 transformer) { - this.transformer = transformer != null ? transformer : Functions.identity(); - } + public static final SystemPropertiesSource DEFAULT = new SystemPropertiesSource(); private SystemPropertiesSource() { - this(null); } @Override public Optional getOptional(String name) { - return Optional.ofNullable(System.getProperty(transformer.call(name))); + return Optional.ofNullable(System.getProperty(name)); } } diff --git a/core/src/test/java/foundation/stack/datamill/configuration/PropertiesTest.java b/core/src/test/java/foundation/stack/datamill/configuration/PropertiesTest.java deleted file mode 100644 index c7b7819..0000000 --- a/core/src/test/java/foundation/stack/datamill/configuration/PropertiesTest.java +++ /dev/null @@ -1,170 +0,0 @@ -package foundation.stack.datamill.configuration; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.contrib.java.lang.system.EnvironmentVariables; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.fail; - -/** - * @author Ravi Chodavarapu (rchodava@gmail.com) - */ -public class PropertiesTest { - @Rule public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); - - @Test - public void environmentVariables() { - environmentVariables.set("test", "value"); - environmentVariables.set("prefix_transformed", "value2"); - - assertEquals("value", Properties.fromEnvironment().get("test").get()); - assertFalse(Properties.fromEnvironment().get("test2").isPresent()); - - assertFalse(Properties.fromEnvironment().get("transformed").isPresent()); - assertEquals("value2", Properties.fromEnvironment(v -> "prefix_" + v).get("transformed").get()); - - assertEquals("value", Properties.fromEnvironment().getRequired("test").asString()); - } - - @Test - public void systemProperties() { - System.setProperty("test", "value"); - System.setProperty("prefix_transformed", "value2"); - - assertEquals("value", Properties.fromSystem().get("test").get()); - assertFalse(Properties.fromSystem().get("test2").isPresent()); - - assertFalse(Properties.fromSystem().get("transformed").isPresent()); - assertEquals("value2", Properties.fromSystem(v -> "prefix_" + v).get("transformed").get()); - - 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()); - assertFalse(Properties.fromFile("test.properties").get("test2").isPresent()); - - assertEquals("value3", Properties.fromFile("test.properties").orFile("test2.properties").get("test3").get()); - assertFalse(Properties.fromFile("test.properties").orFile("test2.properties").get("test4").isPresent()); - - assertEquals("value", Properties.fromFile("test.properties").getRequired("test").asString()); - assertEquals("value3", Properties.fromFile("test.properties").orFile("test2.properties").getRequired("test3").asString()); - } - - @Test - public void defaults() { - System.setProperty("test", "value"); - - assertEquals("value", Properties.fromSystem().orDefaults(d -> d.put("test", "value2")).get("test").get()); - assertEquals("value2", Properties.fromSystem().orDefaults(d -> d.put("test2", "value2")).get("test2").get()); - - assertEquals("value", Properties.fromSystem().orDefaults(d -> d.put("test", "value2")).getRequired("test").asString()); - assertEquals("value2", Properties.fromSystem().orDefaults(d -> d.put("test2", "value2")).getRequired("test2").asString()); - } - - - @Test - public void aliases() { - System.setProperty("test", "value"); - - assertEquals("value", Properties.fromSystem().get("test").get()); - assertEquals("value", Properties.fromSystem().alias("test2", "test").get("test2").get()); - } - - @Test - public void chains() { - environmentVariables.set("test4", "value4"); - System.setProperty("test5", "value5"); - - PropertySource chain = Properties.fromFile("test.properties").orFile("test2.properties").orSystem() - .orEnvironment().orDefaults(d -> d.put("test6", "value6")); - - assertEquals("value", chain.get("test").get()); - assertEquals("value3", chain.get("test3").get()); - assertEquals("value4", chain.get("test4").get()); - assertEquals("value5", chain.get("test5").get()); - assertEquals("value6", chain.get("test6").get()); - - assertEquals("value", chain.getRequired("test").asString()); - assertEquals("value3", chain.getRequired("test3").asString()); - assertEquals("value4", chain.getRequired("test4").asString()); - assertEquals("value5", chain.getRequired("test5").asString()); - assertEquals("value6", chain.getRequired("test6").asString()); - } - - @Test - public void missingRequiredProperties() throws Exception { - environmentVariables.set("test4", "value4"); - System.setProperty("test5", "value5"); - - try { - Properties.fromEnvironment().getRequired("test3"); - fail(); - } catch (IllegalArgumentException e) { - } - - try { - Properties.fromSystem().getRequired("test3"); - fail(); - } catch (IllegalArgumentException e) { - } - - try { - Properties.fromFile("test.properties").getRequired("test3"); - fail(); - } catch (IllegalArgumentException e) { - } - - try { - Properties.fromFile("test.properties").orFile("test2.properties").getRequired("test4"); - fail(); - } catch (IllegalArgumentException e) { - } - - try { - Properties.fromFile("test.properties").orFile("test2.properties").orSystem().orEnvironment() - .orDefaults(d -> d.put("test6", "value6")).getRequired("test2"); - fail(); - } 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"; - } -} diff --git a/core/src/test/java/foundation/stack/datamill/configuration/PropertySourceChainTest.java b/core/src/test/java/foundation/stack/datamill/configuration/PropertySourceChainTest.java new file mode 100644 index 0000000..06d5218 --- /dev/null +++ b/core/src/test/java/foundation/stack/datamill/configuration/PropertySourceChainTest.java @@ -0,0 +1,237 @@ +package foundation.stack.datamill.configuration; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import org.junit.Rule; +import org.junit.Test; +import org.junit.contrib.java.lang.system.EnvironmentVariables; + +import java.io.File; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +/** + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public class PropertySourceChainTest { + @Rule + public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); + + @Test + public void environmentVariables() { + environmentVariables.set("test", "value"); + environmentVariables.set("prefix_transformed", "value2"); + + assertEquals("value", PropertySourceChain.ofEnvironment().get("test").get()); + assertFalse(PropertySourceChain.ofEnvironment().get("test2").isPresent()); + + assertFalse(PropertySourceChain.ofEnvironment().get("transformed").isPresent()); + assertEquals("value2", PropertySourceChain.ofEnvironment(v -> "prefix_" + v).get("transformed").get()); + + assertEquals("value", PropertySourceChain.ofEnvironment().getRequired("test").asString()); + } + + @Test + public void systemProperties() { + System.setProperty("test", "value"); + System.setProperty("prefix_transformed", "value2"); + + assertEquals("value", PropertySourceChain.ofSystem().get("test").get()); + assertFalse(PropertySourceChain.ofSystem().get("test2").isPresent()); + + assertFalse(PropertySourceChain.ofSystem().get("transformed").isPresent()); + assertEquals("value2", PropertySourceChain.ofSystem(v -> "prefix_" + v).get("transformed").get()); + + assertEquals("value", PropertySourceChain.ofSystem().getRequired("test").asString()); + } + + @Test + public void constantClasses() { + assertEquals("value", PropertySourceChain.ofConstantsClass(ConstantsClass.class).get(ConstantsClass.property).get()); + assertFalse(PropertySourceChain.ofConstantsClass(ConstantsClass.class).get("property2").isPresent()); + assertFalse(PropertySourceChain.ofConstantsClass(ConstantsClass.class).get("config/instance").isPresent()); + + assertEquals("publicValue", PropertySourceChain.ofConstantsClass(ConstantsClass.class).get(ConstantsClass.publicProperty).get()); + assertEquals("privateValue", PropertySourceChain.ofConstantsClass(ConstantsClass.class).get(ConstantsClass.privateProperty).get()); + assertEquals("nonFinalValue", PropertySourceChain.ofConstantsClass(ConstantsClass.class).get(ConstantsClass.nonFinalProperty).get()); + assertEquals("1", PropertySourceChain.ofConstantsClass(ConstantsClass.class).get(ConstantsClass.integerProperty).get()); + assertEquals("true", PropertySourceChain.ofConstantsClass(ConstantsClass.class).get(ConstantsClass.booleanProperty).get()); + + assertEquals("value", PropertySourceChain.ofConstantsClass(ConstantsClass.class).getRequired("config/property").asString()); + + assertEquals("ifacePublic", PropertySourceChain.ofConstantsClass(ConstantsClass.class).orConstantsClass(ConstantsInterface.class).get(ConstantsInterface.IFACE_PUBLIC_TEST).get()); + assertEquals("2", PropertySourceChain.ofConstantsClass(ConstantsClass.class).orConstantsClass(ConstantsInterface.class).get(ConstantsInterface.IFACE_INTEGER).get()); + assertEquals("true", PropertySourceChain.ofConstantsClass(ConstantsClass.class).orConstantsClass(ConstantsInterface.class).get(ConstantsInterface.IFACE_BOOLEAN).get()); + assertEquals("true", PropertySourceChain.ofConstantsClass(ConstantsClass.class).orConstantsClass(ConstantsInterface.class).get(ConstantsClass.booleanProperty).get()); + } + + @Test + public void computation() { + assertEquals("computed", PropertySourceChain.ofComputed(name -> "computed").orFile("test.properties").get("test").get()); + assertEquals("value", PropertySourceChain.ofComputed(name -> "test2".equals(name) ? "computed" : null) + .orFile("test.properties").get("test").get()); + } + + @Test + public void files() throws IOException { + File externalProperties = File.createTempFile("test", ".properties"); + try { + Files.write("test4=value4\n", externalProperties, Charsets.UTF_8); + + assertEquals("value", PropertySourceChain.ofFile("test.properties").get("test").get()); + assertFalse(PropertySourceChain.ofFile("test.properties").get("test2").isPresent()); + + assertEquals("value3", PropertySourceChain.ofFile("test.properties").orFile("test2.properties").get("test3").get()); + assertFalse(PropertySourceChain.ofFile("test.properties").orFile("test2.properties").get("test4").isPresent()); + + assertEquals("value", PropertySourceChain.ofFile("test.properties").getRequired("test").asString()); + assertEquals("value3", PropertySourceChain.ofFile("test.properties").orFile("test2.properties").getRequired("test3").asString()); + + assertEquals("value4", PropertySourceChain.ofFile(externalProperties.getPath()).orFile("test2.properties").getRequired("test4").asString()); + } finally { + externalProperties.delete(); + } + } + + @Test + public void immediates() { + System.setProperty("test", "value"); + + assertEquals("value", PropertySourceChain.ofSystem().orImmediate(d -> d.put("test", "value2")).get("test").get()); + assertEquals("value2", PropertySourceChain.ofSystem().orImmediate(d -> d.put("test2", "value2")).get("test2").get()); + + assertEquals("value", PropertySourceChain.ofSystem().orImmediate(d -> d.put("test", "value2")).getRequired("test").asString()); + assertEquals("value2", PropertySourceChain.ofSystem().orImmediate(d -> d.put("test2", "value2")).getRequired("test2").asString()); + + assertEquals("value2", PropertySourceChain.ofImmediate(d -> d.put("test2", "value2")).getRequired("test2").asString()); + } + + @Test + public void chains() { + environmentVariables.set("test4", "value4"); + System.setProperty("test5", "value5"); + + PropertySource chain = PropertySourceChain + .ofFile("test.properties") + .orFile("nonexistent.proeprties") + .orFile("test2.properties").orSystem().orEnvironment() + .orImmediate(p -> p.put("test6", "value6")); + + assertEquals("value", chain.get("test").get()); + assertEquals("value3", chain.get("test3").get()); + assertEquals("value4", chain.get("test4").get()); + assertEquals("value5", chain.get("test5").get()); + assertEquals("value6", chain.get("test6").get()); + + assertEquals("value", chain.getRequired("test").asString()); + assertEquals("value3", chain.getRequired("test3").asString()); + assertEquals("value4", chain.getRequired("test4").asString()); + assertEquals("value5", chain.getRequired("test5").asString()); + assertEquals("value6", chain.getRequired("test6").asString()); + } + + @Test + public void delegating() { + DelegatingPropertySource delegatingSource = new DelegatingPropertySource(); + assertEquals("value2", PropertySourceChain.ofSource(delegatingSource) + .orImmediate(d -> d.put("test2", "value2")).getRequired("test2").asString()); + + delegatingSource.setDelegate(PropertySourceChain.ofComputed(name -> "value")); + assertEquals("value", PropertySourceChain.ofSource(delegatingSource) + .orImmediate(d -> d.put("test2", "value2")).getRequired("test2").asString()); + } + + @Test + public void transformers() { + environmentVariables.set("PROPERTY_NAME", "value4"); + System.setProperty("PROPERTY2_NAME", "value5"); + + PropertySource base = PropertySourceChain + .ofFile("test.properties") + .orFile("test2.properties") + .orSystem() + .orEnvironment() + .orImmediate(d -> d.put("test6", "value6")); + + PropertySource leaf = PropertySourceChain.ofSource(base).orSource(base, PropertySourceChain.Transformers.LEAF); + PropertySource upper = PropertySourceChain.ofSource(base).orSource(base, + PropertySourceChain.Transformers.compose( + PropertySourceChain.Transformers.LEAF, + PropertySourceChain.Transformers.LOWER_CAMEL_TO_UPPER_UNDERSCORE)); + + assertEquals("value", leaf.get("category/test").get()); + assertEquals("value3", leaf.get("category/test3").get()); + assertEquals("value4", leaf.get("category/PROPERTY_NAME").get()); + assertEquals("value5", leaf.get("category/PROPERTY2_NAME").get()); + assertEquals("value6", leaf.get("category/test6").get()); + + assertEquals("value4", upper.getRequired("category/subcategory/propertyName").asString()); + assertEquals("value5", upper.getRequired("category/subcategory/property2Name").asString()); + } + + @Test + public void missingRequiredProperties() throws Exception { + environmentVariables.set("test4", "value4"); + System.setProperty("test5", "value5"); + + try { + PropertySourceChain.ofEnvironment().getRequired("test3"); + fail(); + } catch (IllegalArgumentException e) { + } + + try { + PropertySourceChain.ofSystem().getRequired("test3"); + fail(); + } catch (IllegalArgumentException e) { + } + + try { + PropertySourceChain.ofFile("test.properties").getRequired("test3"); + fail(); + } catch (IllegalArgumentException e) { + } + + try { + PropertySourceChain.ofFile("test.properties").orFile("test2.properties").getRequired("test4"); + fail(); + } catch (IllegalArgumentException e) { + } + + try { + PropertySourceChain.ofFile("test.properties").orFile("test2.properties").orSystem().orEnvironment() + .orImmediate(d -> d.put("test6", "value6")).getRequired("test2"); + fail(); + } 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"; + } +} diff --git a/core/src/test/java/foundation/stack/datamill/configuration/WiringTest.java b/core/src/test/java/foundation/stack/datamill/configuration/WiringTest.java index 824480f..3d890b2 100644 --- a/core/src/test/java/foundation/stack/datamill/configuration/WiringTest.java +++ b/core/src/test/java/foundation/stack/datamill/configuration/WiringTest.java @@ -313,7 +313,7 @@ public void conveniences() { @Test public void namedValuesFromPropertySource() { Wiring wiring = new Wiring() - .setNamedPropertySource(Properties.fromSystem().orDefaults(d -> d + .setNamedPropertySource(PropertySourceChain.ofSystem().orImmediate(d -> d .put("boolean", "true") .put("booleanWrapper", "true") .put("byte", "1") diff --git a/cucumber/src/main/java/foundation/stack/datamill/cucumber/PlaceholderResolver.java b/cucumber/src/main/java/foundation/stack/datamill/cucumber/PlaceholderResolver.java index 962651f..8011378 100644 --- a/cucumber/src/main/java/foundation/stack/datamill/cucumber/PlaceholderResolver.java +++ b/cucumber/src/main/java/foundation/stack/datamill/cucumber/PlaceholderResolver.java @@ -1,7 +1,7 @@ package foundation.stack.datamill.cucumber; import com.jayway.jsonpath.JsonPath; -import foundation.stack.datamill.configuration.Properties; +import foundation.stack.datamill.configuration.PropertySourceChain; import foundation.stack.datamill.values.Value; import foundation.stack.datamill.security.impl.BCrypt; import org.slf4j.Logger; @@ -129,7 +129,7 @@ private String resolveDateFormatterPlaceholder(String key) { private String resolveSystemPlaceholder(String key) { if (key.startsWith(SYSTEM_PLACEHOLDER_PREFIX)) { key = key.substring(SYSTEM_PLACEHOLDER_PREFIX.length()); - return Properties.fromSystem().get(key).orElse(null); + return PropertySourceChain.ofSystem().get(key).orElse(null); } return null; diff --git a/lambda-api/src/main/java/foundation/stack/lambda/ApiHandler.java b/lambda-api/src/main/java/foundation/stack/lambda/ApiHandler.java index bdc9f48..64360a4 100644 --- a/lambda-api/src/main/java/foundation/stack/lambda/ApiHandler.java +++ b/lambda-api/src/main/java/foundation/stack/lambda/ApiHandler.java @@ -1,6 +1,7 @@ package foundation.stack.lambda; import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; import com.google.common.base.Charsets; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; @@ -28,7 +29,7 @@ /** * @author Ravi Chodavarapu (rchodava@gmail.com) */ -public abstract class ApiHandler { +public abstract class ApiHandler implements RequestStreamHandler { private static final String BODY_PROPERTY = "body"; private static final String LOG_LEVEL_PROPERTY = "logLevel"; private static final String HEADERS_PROPERTY = "headers"; @@ -188,7 +189,8 @@ private JSONObject handle(JSONObject requestJson) { } } - public final void handle(InputStream requestStream, OutputStream responseStream, Context __) { + @Override + public final void handleRequest(InputStream requestStream, OutputStream responseStream, Context __) { setLogLevel(); ServerRequest request = null; diff --git a/lambda-api/src/test/java/foundation/stack/lambda/ApiHandlerTest.java b/lambda-api/src/test/java/foundation/stack/lambda/ApiHandlerTest.java index e7d19f6..d549ad9 100644 --- a/lambda-api/src/test/java/foundation/stack/lambda/ApiHandlerTest.java +++ b/lambda-api/src/test/java/foundation/stack/lambda/ApiHandlerTest.java @@ -70,7 +70,7 @@ public void handle() { // Test valid request ByteArrayOutputStream output = new ByteArrayOutputStream(); TestApiHandler handler = new TestApiHandler(); - handler.handle(new ByteArrayInputStream(("{" + + handler.handleRequest(new ByteArrayInputStream(("{" + "\"path\": \"/test\", " + "\"httpMethod\": \"GET\", " + "\"headers\": {" + @@ -92,7 +92,7 @@ public void handle() { // Request with no HTTP method output = new ByteArrayOutputStream(); - handler.handle(new ByteArrayInputStream(("{ \"path\": \"/test\" }").getBytes(Charsets.UTF_8)), output, null); + handler.handleRequest(new ByteArrayInputStream(("{ \"path\": \"/test\" }").getBytes(Charsets.UTF_8)), output, null); response = new JSONObject(new String(output.toByteArray(), Charsets.UTF_8)); assertEquals(400, response.optInt("statusCode")); @@ -100,7 +100,7 @@ public void handle() { // Test a route that returns a null response output = new ByteArrayOutputStream(); - handler.handle(new ByteArrayInputStream(("{" + + handler.handleRequest(new ByteArrayInputStream(("{" + "\"path\": \"/null\", " + "\"httpMethod\": \"GET\"" + "}") @@ -112,7 +112,7 @@ public void handle() { // Test a request with an uknown path output = new ByteArrayOutputStream(); - handler.handle(new ByteArrayInputStream(("{" + + handler.handleRequest(new ByteArrayInputStream(("{" + "\"path\": \"/other\", " + "\"httpMethod\": \"GET\"" + "}") @@ -124,7 +124,7 @@ public void handle() { // Test a route that results in an error being emitted, that isn't handled output = new ByteArrayOutputStream(); - handler.handle(new ByteArrayInputStream(("{" + + handler.handleRequest(new ByteArrayInputStream(("{" + "\"path\": \"/error\", " + "\"httpMethod\": \"GET\"" + "}") @@ -136,7 +136,7 @@ public void handle() { // Test a route that results in an error being thrown output = new ByteArrayOutputStream(); - handler.handle(new ByteArrayInputStream(("{" + + handler.handleRequest(new ByteArrayInputStream(("{" + "\"path\": \"/unhandled\", " + "\"httpMethod\": \"GET\"" + "}") From ef38c251ea3d835f258595b04e6b1dd7a87ac712 Mon Sep 17 00:00:00 2001 From: Ravi Chodavarapu Date: Sun, 16 Apr 2017 18:19:48 +0100 Subject: [PATCH 4/5] Reimplemented Wirings using a fresh and simpler approach of a Wiring composed of a PropertySourceChain and a FactoryChain Wirings now support qualifiers and scopes Wirings now report much more detail when they fail to construct instances --- .../datamill/configuration/BooleanValue.java | 20 - .../stack/datamill/configuration/Factory.java | 18 + .../datamill/configuration/FactoryChain.java | 66 ++ .../datamill/configuration/FactoryChains.java | 79 ++ .../ImmediatePropertySource.java | 3 +- .../datamill/configuration/IntegerValue.java | 20 - .../stack/datamill/configuration/Module.java | 10 - .../stack/datamill/configuration/Named.java | 2 +- .../PropertyNameTransformers.java | 55 ++ .../configuration/PropertySource.java | 6 +- .../configuration/PropertySourceChain.java | 319 ++------ .../PropertySourceChainContinuation.java | 99 --- .../configuration/PropertySources.java | 151 ++++ .../datamill/configuration/Qualifier.java | 18 + .../datamill/configuration/Qualifiers.java | 16 + .../configuration/QualifyingFactory.java | 19 + .../stack/datamill/configuration/Scope.java | 14 + .../configuration/SingletonScope.java | 29 + .../configuration/TypeLessFactory.java | 13 + .../{StringValue.java => Value.java} | 4 +- .../stack/datamill/configuration/Wiring.java | 740 +++--------------- .../configuration/WiringException.java | 55 ++ .../configuration/impl/AbstractSource.java | 7 +- .../datamill/configuration/impl/Classes.java | 2 +- .../impl/ConcreteClassFactory.java | 166 ++++ .../impl/ConstantsClassSource.java | 20 +- .../configuration/impl/FactoryChainImpl.java | 124 +++ .../impl/ImmediatePropertySourceImpl.java | 17 + .../impl/NamedParameterValueRetriever.java | 57 ++ .../impl/PropertySourceChainImpl.java | 130 +++ .../impl/SimpleValueConverter.java | 246 ++++++ .../configuration/FactoryChainsTest.java | 115 +++ .../PropertySourceChainTest.java | 237 ------ .../configuration/PropertySourcesTest.java | 243 ++++++ .../datamill/configuration/WiringTest.java | 463 ++--------- .../impl/ConcreteClassFactoryTest.java | 188 +++++ .../impl/SimpleValueConverterTest.java | 53 ++ .../cucumber/PlaceholderResolver.java | 4 +- 38 files changed, 2100 insertions(+), 1728 deletions(-) delete mode 100644 core/src/main/java/foundation/stack/datamill/configuration/BooleanValue.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/Factory.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/FactoryChain.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/FactoryChains.java delete mode 100644 core/src/main/java/foundation/stack/datamill/configuration/IntegerValue.java delete mode 100644 core/src/main/java/foundation/stack/datamill/configuration/Module.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/PropertyNameTransformers.java delete mode 100644 core/src/main/java/foundation/stack/datamill/configuration/PropertySourceChainContinuation.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/PropertySources.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/Qualifier.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/Qualifiers.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/QualifyingFactory.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/Scope.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/SingletonScope.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/TypeLessFactory.java rename core/src/main/java/foundation/stack/datamill/configuration/{StringValue.java => Value.java} (80%) create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/WiringException.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/impl/ConcreteClassFactory.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/impl/FactoryChainImpl.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/impl/NamedParameterValueRetriever.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/impl/PropertySourceChainImpl.java create mode 100644 core/src/main/java/foundation/stack/datamill/configuration/impl/SimpleValueConverter.java create mode 100644 core/src/test/java/foundation/stack/datamill/configuration/FactoryChainsTest.java delete mode 100644 core/src/test/java/foundation/stack/datamill/configuration/PropertySourceChainTest.java create mode 100644 core/src/test/java/foundation/stack/datamill/configuration/PropertySourcesTest.java create mode 100644 core/src/test/java/foundation/stack/datamill/configuration/impl/ConcreteClassFactoryTest.java create mode 100644 core/src/test/java/foundation/stack/datamill/configuration/impl/SimpleValueConverterTest.java diff --git a/core/src/main/java/foundation/stack/datamill/configuration/BooleanValue.java b/core/src/main/java/foundation/stack/datamill/configuration/BooleanValue.java deleted file mode 100644 index 4dcf129..0000000 --- a/core/src/main/java/foundation/stack/datamill/configuration/BooleanValue.java +++ /dev/null @@ -1,20 +0,0 @@ -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/Factory.java b/core/src/main/java/foundation/stack/datamill/configuration/Factory.java new file mode 100644 index 0000000..46b4597 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/Factory.java @@ -0,0 +1,18 @@ +package foundation.stack.datamill.configuration; + +import rx.functions.Func2; + +/** + * A factory for constructing objects of particular types. Use {@link FactoryChains} to create factories and compose + * them together into chains. Note that the contract of a factory is to return a fully constructed instance of the + * requested type (it can use the provided {@link Wiring} to construct the instance, or return null if the factory + * cannot construct an instance. + * + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +@FunctionalInterface +public interface Factory extends Func2, R> { + static Factory wrap(TypeLessFactory factory) { + return (w, c) -> factory.call(w); + } +} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/FactoryChain.java b/core/src/main/java/foundation/stack/datamill/configuration/FactoryChain.java new file mode 100644 index 0000000..5663f60 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/FactoryChain.java @@ -0,0 +1,66 @@ +package foundation.stack.datamill.configuration; + +/** + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public interface FactoryChain extends QualifyingFactory { + /** + * Add a factory which will be used to attempt construction of instances for all types. + */ + FactoryChain thenForAny(Factory factory); + + /** + * Add a factory which will be used to attempt construction of instances of concrete classes. + */ + FactoryChain thenForAnyConcreteClass(); + + /** + * Add a factory for a type, and it's super-classes and interfaces. Note that this factory will be invoked if the + * type requested is exactly the specified type, or one of it's super-classes or interfaces. + */ + FactoryChain thenForSuperOf(Class type, Factory factory); + + /** + * Add a factory for a specific type. Note that this factory is only invoked if the type being constructed matches + * the exact type specified. + * + * @param type Exact type for which the specified factory will be used. + */ + FactoryChain thenForType(Class type, Factory factory); + + /** + * @see #thenForAny(Factory) + */ + FactoryChain thenForAny(TypeLessFactory factory); + + /** + * @see #thenForSuperOf(Class, Factory) + */ + FactoryChain thenForSuperOf(Class type, TypeLessFactory factory); + + /** + * @see #thenForType(Class, Factory) + */ + FactoryChain thenForType(Class type, TypeLessFactory factory); + + /** + * Add a qualifying factory which will be used to attempt construction of instances for all types. + * + * @see #thenForAny(Factory) + */ + FactoryChain thenForAny(QualifyingFactory factory); + + /** + * Add a qualifying factory for a type, and it's super-classes and interfaces. + * + * @see #thenForSuperOf(Class, Factory) + */ + FactoryChain thenForSuperOf(Class type, QualifyingFactory factory); + + /** + * Add a qualifying factory for a specific type. + * + * @see #thenForType(Class, Factory) + */ + FactoryChain thenForType(Class type, QualifyingFactory factory); +} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/FactoryChains.java b/core/src/main/java/foundation/stack/datamill/configuration/FactoryChains.java new file mode 100644 index 0000000..715766f --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/FactoryChains.java @@ -0,0 +1,79 @@ +package foundation.stack.datamill.configuration; + +import foundation.stack.datamill.configuration.impl.ConcreteClassFactory; +import foundation.stack.datamill.configuration.impl.FactoryChainImpl; + +/** + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public class FactoryChains { + /** + * @see FactoryChain#thenForAnyConcreteClass() + */ + public static FactoryChain forAnyConcreteClass() { + return forAny(ConcreteClassFactory.instance()); + } + + /** + * @see FactoryChain#thenForAny(Factory) + */ + public static FactoryChain forAny(Factory factory) { + return new FactoryChainImpl(FactoryChainImpl.tautology(), factory); + } + + /** + * @see FactoryChain#thenForSuperOf(Class, Factory) + */ + public static FactoryChain forSuperOf(Class type, Factory factory) { + return new FactoryChainImpl(FactoryChainImpl.isSuperOf(type), factory); + } + + /** + * @see FactoryChain#thenForType(Class, Factory) + */ + public static FactoryChain forType(Class type, Factory factory) { + return new FactoryChainImpl(FactoryChainImpl.isExactType(type), factory); + } + + /** + * @see FactoryChain#thenForAny(QualifyingFactory) + */ + public static FactoryChain forAny(QualifyingFactory factory) { + return new FactoryChainImpl(FactoryChainImpl.tautology(), factory); + } + + /** + * @see FactoryChain#thenForSuperOf(Class, QualifyingFactory) + */ + public static FactoryChain forSuperOf(Class type, QualifyingFactory factory) { + return new FactoryChainImpl(FactoryChainImpl.isSuperOf(type), factory); + } + + /** + * @see FactoryChain#thenForType(Class, QualifyingFactory) + */ + public static FactoryChain forType(Class type, QualifyingFactory factory) { + return new FactoryChainImpl(FactoryChainImpl.isExactType(type), factory); + } + + /** + * @see FactoryChain#thenForAny(TypeLessFactory) + */ + public static FactoryChain forAny(TypeLessFactory factory) { + return new FactoryChainImpl(FactoryChainImpl.tautology(), factory); + } + + /** + * @see FactoryChain#thenForSuperOf(Class, TypeLessFactory) + */ + public static FactoryChain forSuperOf(Class type, TypeLessFactory factory) { + return new FactoryChainImpl(FactoryChainImpl.isSuperOf(type), factory); + } + + /** + * @see FactoryChain#thenForType(Class, TypeLessFactory) + */ + public static FactoryChain forType(Class type, TypeLessFactory factory) { + return new FactoryChainImpl(FactoryChainImpl.isExactType(type), factory); + } +} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/ImmediatePropertySource.java b/core/src/main/java/foundation/stack/datamill/configuration/ImmediatePropertySource.java index 6542e55..c1ab60f 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/ImmediatePropertySource.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/ImmediatePropertySource.java @@ -3,9 +3,10 @@ /** * Used in defining an immediate immutable set of properties. * - * @see PropertySourceChain + * @see PropertySources * @author Ravi Chodavarapu (rchodava@gmail.com) */ public interface ImmediatePropertySource { ImmediatePropertySource put(String name, String value); + ImmediatePropertySource put(String name, String format, Object... arguments); } diff --git a/core/src/main/java/foundation/stack/datamill/configuration/IntegerValue.java b/core/src/main/java/foundation/stack/datamill/configuration/IntegerValue.java deleted file mode 100644 index 0080320..0000000 --- a/core/src/main/java/foundation/stack/datamill/configuration/IntegerValue.java +++ /dev/null @@ -1,20 +0,0 @@ -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/Module.java b/core/src/main/java/foundation/stack/datamill/configuration/Module.java deleted file mode 100644 index 75316cb..0000000 --- a/core/src/main/java/foundation/stack/datamill/configuration/Module.java +++ /dev/null @@ -1,10 +0,0 @@ -package foundation.stack.datamill.configuration; - -import rx.functions.Action1; - -/** - * @author Ravi Chodavarapu (rchodava@gmail.com) - */ -@FunctionalInterface -public interface Module extends Action1 { -} 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 4a3b081..7b938ce 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/Named.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/Named.java @@ -3,7 +3,7 @@ import java.lang.annotation.*; /** - * Annotation to be used to add a name qualifier to constructor parameters. + * Annotation to be used to request a named property to be injected into a constructor parameter. * * @see Wiring * @author Ravi Chodavarapu (rchodava@gmail.com) diff --git a/core/src/main/java/foundation/stack/datamill/configuration/PropertyNameTransformers.java b/core/src/main/java/foundation/stack/datamill/configuration/PropertyNameTransformers.java new file mode 100644 index 0000000..84ed9bc --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/PropertyNameTransformers.java @@ -0,0 +1,55 @@ +package foundation.stack.datamill.configuration; + +import com.google.common.base.CaseFormat; +import rx.functions.Func1; + +/** + * Some common property name transformers that are useful when creating {@link PropertySources}. + */ +public class PropertyNameTransformers { /** + * Transforms from lower-camel case to upper-underscore case, for example: propertyName -> PROPERTY_NAME. + */ + public static final Func1 LOWER_CAMEL_TO_UPPER_UNDERSCORE = + name -> CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, name); + + /** + * Same as calling {@link #leaf(char)} with '/'. + */ + public static final Func1 LEAF = leaf('/'); + + /** + *

+ * Returns a transformation function that applies the first function, and then the second function on the result + * of the first. So compose(LEAF, LOWER_CAMEL_TO_UPPER_UNDERSCORE) will return a function that transforms: + *

+ *

+ * category/propertyName -> PROPERTY_NAME + *

+ */ + public static final Func1 compose(Func1 a, Func1 b) { + return name -> b.call(a.call(name)); + } + + /** + *

+ * Returns a transformation function that returns the leaf name of a name separated by the specified separator + * character. For example: + *

+ *

+ * leaf('/') returns a function which transforms: category/propertyName -> propertyName + * leaf(':') returns a function which transforms: category:propertyName -> propertyName + *

+ */ + public static final Func1 leaf(char separator) { + return name -> { + if (name != null) { + int leafSeparator = name.lastIndexOf('/'); + if (leafSeparator > 0) { + return name.substring(leafSeparator + 1); + } + } + + return name; + }; + } +} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/PropertySource.java b/core/src/main/java/foundation/stack/datamill/configuration/PropertySource.java index 8b72d5c..0669131 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/PropertySource.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/PropertySource.java @@ -1,12 +1,12 @@ package foundation.stack.datamill.configuration; import foundation.stack.datamill.values.Value; -import rx.functions.Action1; +import rx.functions.Func1; import java.util.Optional; /** - * A source of properties. Use {@link PropertySourceChain} to create {@link PropertySource}s. + * A source of properties. Use {@link PropertySources} to create {@link PropertySource}s. * * @author Ravi Chodavarapu (rchodava@gmail.com) */ @@ -34,5 +34,5 @@ public interface PropertySource { * * @param propertiesConsumer Lambda that receives this property source. */ - PropertySource with(Action1 propertiesConsumer); + R with(Func1 propertiesConsumer); } 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 0a66d87..bd6df5c 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/PropertySourceChain.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/PropertySourceChain.java @@ -1,320 +1,99 @@ package foundation.stack.datamill.configuration; -import com.google.common.base.CaseFormat; -import foundation.stack.datamill.configuration.impl.*; import rx.functions.Action1; import rx.functions.Func1; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - /** - *

- * Allows {@link PropertySource}s to be composed together into a chain so that alternative sources of desired properties - * can be specified when looking up configuration properties. For example, consider a chain composed and used as follows: - *

- *

- * chain = PropertySourceChain.ofFile("service.properties").orFile("defaults.properties").orEnvironment()
- *     .orImmediate(defaults -> defaults.put("property1", "value1").put("property2", "value2"));
- *
- * chain.getRequired("property1");
- * chain.getRequired("property3");
- * 
- *

- * This chain will first attempt to retrieve requested properties from the service.properties file. It will consult the - * defaults.properties file next if it can't find properties in service.properties. Subsequently, it will attempt to - * look for those properties amongst environment variables if it still fails to find a requested property. Finally, - * it will return the defaults specified by the orImmediate(...) call. - *

- *

- * Note that each of the methods accepts a transformer to transform the names of the properties before looking up - * the property in that particular property source. See {@link Transformers} for some useful common transformers. - *

- *

- * Note that each of the chaining methods returns a new chain with the additional property source added. This allows you - * to have common chains which you can combine in different ways, because each of those combinations is a new chain. For - * example: - *

- *
- * chain1 = COMMON.orEnvironment(property -> CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, property));
- * chain2 = COMMON.orFile("defaults.properties");
- * 
- *

- * Each of these chains is a separate chain that has some common section. - *

- * * @author Ravi Chodavarapu (rchodava@gmail.com) */ -public final class PropertySourceChain extends AbstractSource implements PropertySourceChainContinuation { +public interface PropertySourceChain extends PropertySource { /** - * @see PropertySourceChainContinuation#orConstantsClass(Class) + * Add a custom property source to the chain at this point. + * + * @param source Source to add to the chain. */ - public static PropertySourceChainContinuation ofConstantsClass(Class constantsClass) { - return ofSource(new ConstantsClassSource<>(constantsClass)); - } + PropertySourceChain or(PropertySource source); /** - * @see PropertySourceChainContinuation#orConstantsClass(Class, Func1) + * @See PropertySources + * @see #or(PropertySource) */ - public static PropertySourceChainContinuation ofConstantsClass(Class constantsClass, Func1 transformer) { - return ofSource(new ConstantsClassSource<>(constantsClass), transformer); - } + PropertySourceChain or(PropertySource source, Func1 transformer); /** - * @see PropertySourceChainContinuation#orComputed(Func1) + * Add a property source to the chain which computes the value of property using the specified function. The + * function can return either a value, or a null to indicate it cannot compute the value of the property - in this + * case, the chain will look to the next source. + * + * @param computation Computation function for the source. */ - public static PropertySourceChainContinuation ofComputed(Func1 computation) { - return ofComputed(computation, null); - } + PropertySourceChain orComputed(Func1 computation); /** - * @see PropertySourceChainContinuation#orComputed(Func1, Func1) + * @See PropertySources + * @see #orComputed(Func1) */ - public static PropertySourceChainContinuation ofComputed(Func1 computation, Func1 transformer) { - return ofSource(new ComputedSource(computation), transformer); - } - - public static PropertySourceChainContinuation ofFile(String path) { - return ofFile(path, null); - } + PropertySourceChain orComputed(Func1 computation, Func1 transformer); /** - * @see PropertySourceChainContinuation#orFile(String) + * Add a property source to the chain which retrieves properties from a constants interface or class. The interface + * or class is expected to define String constants that are annotated with {@link Value} annotations. + * + * @param constantsClass Constants class to add as a source. */ - public static PropertySourceChainContinuation ofFile(String path, Func1 transformer) { - try { - return ofSource(new FileSource(path), transformer); - } catch (IOException e) { - return ofSource(EmptySource.INSTANCE); - } - } + PropertySourceChain orConstantsClass(Class constantsClass); /** - * @see PropertySourceChainContinuation#orImmediate(Action1) + * @See PropertySources + * @see #orConstantsClass(Class) */ - public static PropertySourceChainContinuation ofImmediate(Action1 initializer) { - return ofImmediate(initializer, null); - } + PropertySourceChain orConstantsClass(Class constantsClass, Func1 transformer); /** - * @see PropertySourceChainContinuation#orImmediate(Action1, Func1) + * Add a source which looks up properties defined as environment variables to the chain. */ - public static PropertySourceChainContinuation ofImmediate(Action1 initializer, Func1 transformer) { - ImmediatePropertySourceImpl immediateSource = new ImmediatePropertySourceImpl(); - initializer.call(immediateSource); - - return ofSource(immediateSource, transformer); - } + PropertySourceChain orEnvironment(); /** - * @see PropertySourceChainContinuation#orSource(PropertySource) + * @See PropertySources + * @see #orEnvironment() */ - public static PropertySourceChainContinuation ofSource(PropertySource source) { - return new PropertySourceChain(source, null); - } + PropertySourceChain orEnvironment(Func1 transformer); /** - * @see PropertySourceChainContinuation#orSource(PropertySource, Func1) + * Add a property source to the chain which retrieves properties specified in the file at the specified path. + * + * @param path Path to properties file to add as a source. */ - public static PropertySourceChainContinuation ofSource(PropertySource source, Func1 transformer) { - return new PropertySourceChain(source, transformer); - } + PropertySourceChain orFile(String path); /** - * @see PropertySourceChainContinuation#orEnvironment() + * @See PropertySources + * @see #orFile(String) */ - public static PropertySourceChainContinuation ofEnvironment() { - return ofEnvironment(null); - } + PropertySourceChain orFile(String path, Func1 transformer); /** - * @see PropertySourceChainContinuation#orEnvironment(Func1) + * Add immediate set of properties that can be looked up at this point in the chain. + * + * @param initializer Function which sets up immediate properties at this point in the chain. */ - public static PropertySourceChainContinuation ofEnvironment(Func1 transformer) { - return ofSource(EnvironmentPropertiesSource.DEFAULT, transformer); - } + PropertySourceChain orImmediate(Action1 initializer); /** - * @see PropertySourceChainContinuation#orSystem() + * @See PropertySources + * @see #orImmediate(Action1) */ - public static PropertySourceChainContinuation ofSystem() { - return ofSystem(null); - } + PropertySourceChain orImmediate(Action1 initializer, Func1 transformer); /** - * @see PropertySourceChainContinuation#orSystem(Func1) + * Add a source which looks up properties defined as system properties to the chain. */ - public static PropertySourceChainContinuation ofSystem(Func1 transformer) { - return ofSource(SystemPropertiesSource.DEFAULT, transformer); - } - - private final List chain; - - private PropertySourceChain(PropertySource initialSource, Func1 transformer) { - chain = Collections.singletonList(new TransformedSource(initialSource, transformer)); - } - - private PropertySourceChain(List chain) { - this.chain = chain; - } - - @Override - public Optional getOptional(String name) { - for (TransformedSource source : chain) { - Optional value = source.propertySource.get(source.transformer.call(name)); - if (value.isPresent()) { - return value; - } - } - - return Optional.empty(); - } - - @Override - public PropertySourceChain orComputed(Func1 computation) { - return orSource(new ComputedSource(computation), null); - } - - @Override - public PropertySourceChain orComputed(Func1 computation, Func1 transformer) { - return orSource(new ComputedSource(computation), transformer); - } - - @Override - public PropertySourceChain orConstantsClass(Class constantsClass) { - return orConstantsClass(constantsClass, null); - } - - @Override - public PropertySourceChain orConstantsClass(Class constantsClass, Func1 transformer) { - return orSource(new ConstantsClassSource<>(constantsClass), transformer); - } - - @Override - public PropertySourceChain orImmediate(Action1 initializer) { - return orImmediate(initializer, null); - } - - @Override - public PropertySourceChain orImmediate(Action1 initializer, Func1 transformer) { - ImmediatePropertySourceImpl defaults = new ImmediatePropertySourceImpl(); - initializer.call(defaults); - - return orSource(defaults, transformer); - } - - @Override - public PropertySourceChain orFile(String path) { - return orFile(path, null); - } - - @Override - public PropertySourceChain orFile(String path, Func1 transformer) { - try { - return orSource(new FileSource(path), transformer); - } catch (IOException e) { - return orSource(EmptySource.INSTANCE); - } - } - - @Override - public PropertySourceChain orEnvironment() { - return orEnvironment(null); - } - - @Override - public PropertySourceChain orEnvironment(Func1 transformer) { - return orSource(EnvironmentPropertiesSource.DEFAULT, transformer); - } - - @Override - public PropertySourceChain orSource(PropertySource source) { - return orSource(source, null); - } - - @Override - public PropertySourceChain orSource(PropertySource source, Func1 transformer) { - ArrayList newChain = new ArrayList<>(chain); - newChain.add(new TransformedSource(source, transformer)); - - return new PropertySourceChain(newChain); - } - - @Override - public PropertySourceChain orSystem() { - return orSystem(null); - } - - @Override - public PropertySourceChain orSystem(Func1 transformer) { - return orSource(SystemPropertiesSource.DEFAULT, transformer); - } - - private static class TransformedSource { - private final PropertySource propertySource; - private final Func1 transformer; - - public TransformedSource(PropertySource propertySource, Func1 transformer) { - this.propertySource = propertySource; - this.transformer = transformer != null ? transformer : Transformers.IDENTITY; - } - } + PropertySourceChain orSystem(); /** - * Some common transformers that are useful when creating {@link PropertySourceChain}. + * @See PropertySources + * @see #orSystem() */ - public static class Transformers { - private static final Func1 IDENTITY = name -> name; - - /** - * Transforms from lower-camel case to upper-underscore case, for example: propertyName -> PROPERTY_NAME. - */ - public static final Func1 LOWER_CAMEL_TO_UPPER_UNDERSCORE = - name -> CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, name); - - /** - * Same as calling {@link #leaf(char)} with '/'. - */ - public static final Func1 LEAF = leaf('/'); - - /** - *

- * Returns a transformation function that applies the first function, and then the second function on the result - * of the first. So compose(LEAF, LOWER_CAMEL_TO_UPPER_UNDERSCORE) will return a function that transforms: - *

- *

- * category/propertyName -> PROPERTY_NAME - *

- */ - public static final Func1 compose(Func1 a, Func1 b) { - return name -> b.call(a.call(name)); - } - - /** - *

- * Returns a transformation function that returns the leaf name of a name separated by the specified separator - * character. For example: - *

- *

- * leaf('/') returns a function which transforms: category/propertyName -> propertyName - * leaf(':') returns a function which transforms: category:propertyName -> propertyName - *

- */ - public static final Func1 leaf(char separator) { - return name -> { - if (name != null) { - int leafSeparator = name.lastIndexOf('/'); - if (leafSeparator > 0) { - return name.substring(leafSeparator + 1); - } - } - - return name; - }; - } - } - + PropertySourceChain orSystem(Func1 transformer); } diff --git a/core/src/main/java/foundation/stack/datamill/configuration/PropertySourceChainContinuation.java b/core/src/main/java/foundation/stack/datamill/configuration/PropertySourceChainContinuation.java deleted file mode 100644 index f271855..0000000 --- a/core/src/main/java/foundation/stack/datamill/configuration/PropertySourceChainContinuation.java +++ /dev/null @@ -1,99 +0,0 @@ -package foundation.stack.datamill.configuration; - -import rx.functions.Action1; -import rx.functions.Func1; - -/** - * @author Ravi Chodavarapu (rchodava@gmail.com) - */ -public interface PropertySourceChainContinuation extends PropertySource { - /** - * Add a property source to the chain which computes the value of property using the specified function. The - * function can return either a value, or a null to indicate it cannot compute the value of the property - in this - * case, the chain will look to the next source. - * - * @param computation Computation function for the source. - */ - PropertySourceChain orComputed(Func1 computation); - - /** - * @See PropertySourceChain - * @see #orComputed(Func1) - */ - PropertySourceChain orComputed(Func1 computation, Func1 transformer); - - /** - * Add a property source to the chain which retrieves properties from a constants interface or class. The interface - * or class is expected to define String constants that are annotated with {@link StringValue} annotations. - * - * @param constantsClass Constants class to add as a source. - */ - PropertySourceChain orConstantsClass(Class constantsClass); - - /** - * @See PropertySourceChain - * @see #orConstantsClass(Class) - */ - PropertySourceChain orConstantsClass(Class constantsClass, Func1 transformer); - - /** - * Add immediate set of properties that can be looked up at this point in the chain. - * - * @param initializer Function which sets up immediate properties at this point in the chain. - */ - PropertySourceChain orImmediate(Action1 initializer); - - /** - * @See PropertySourceChain - * @see #orImmediate(Action1) - */ - PropertySourceChain orImmediate(Action1 initializer, Func1 transformer); - - /** - * Add a property source to the chain which retrieves properties specified in the file at the specified path. - * - * @param path Path to properties file to add as a source. - */ - PropertySourceChain orFile(String path); - - /** - * @See PropertySourceChain - * @see #orFile(String) - */ - PropertySourceChain orFile(String path, Func1 transformer); - - /** - * Add a source which looks up properties defined as environment variables to the chain. - */ - PropertySourceChain orEnvironment(); - - /** - * @See PropertySourceChain - * @see #orEnvironment() - */ - PropertySourceChain orEnvironment(Func1 transformer); - - /** - * Add a custom property source to the chain at this point. - * - * @param source Source to add to the chain. - */ - PropertySourceChain orSource(PropertySource source); - - /** - * @See PropertySourceChain - * @see #orSource(PropertySource) - */ - PropertySourceChain orSource(PropertySource source, Func1 transformer); - - /** - * Add a source which looks up properties defined as system properties to the chain. - */ - PropertySourceChain orSystem(); - - /** - * @See PropertySourceChain - * @see #orSystem() - */ - PropertySourceChain orSystem(Func1 transformer); -} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/PropertySources.java b/core/src/main/java/foundation/stack/datamill/configuration/PropertySources.java new file mode 100644 index 0000000..fb2e383 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/PropertySources.java @@ -0,0 +1,151 @@ +package foundation.stack.datamill.configuration; + +import foundation.stack.datamill.configuration.impl.*; +import rx.functions.Action1; +import rx.functions.Func1; + +import java.io.IOException; + +/** + *

+ * Use this class to create some common {@link PropertySource}s. This class can also be used to compose together + * {@link PropertySource}s into {@link PropertySourceChain}s so that alternative sources of desired properties can be + * specified when looking up configuration properties. For example, consider a chain composed and used as follows: + *

+ *

+ * chain = PropertySources.fromFile("service.properties").orFile("defaults.properties").orEnvironment()
+ *     .orImmediate(defaults -> defaults.put("property1", "value1").put("property2", "value2"));
+ *
+ * chain.getRequired("property1");
+ * chain.getRequired("property3");
+ * 
+ *

+ * This chain will first attempt to retrieve the requested properties from the service.properties file. It will consult + * the defaults.properties file next if it can't find properties in service.properties. Subsequently, it will attempt to + * look for those properties amongst environment variables if it still fails to find a requested property. Finally, + * it will return the defaults specified by the orImmediate(...) call. + *

+ *

+ * Note that each of the methods accepts a transformer to transform the names of the properties before looking up + * the property in that particular property source. See {@link PropertyNameTransformers} for some useful common + * transformers. + *

+ *

+ * Note that each of the chaining methods returns a new chain with the additional property source added. This allows you + * to have common chains which you can combine in different ways, because each of those combinations is a new chain. For + * example: + *

+ *
+ * chain1 = COMMON.orEnvironment(PropertyNameTransformers.LOWER_CAMEL_TO_UPPER_UNDERSCORE);
+ * chain2 = COMMON.orFile("defaults.properties");
+ * 
+ *

+ * Each of these chains is a separate chain that has some common section. + *

+ * + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public final class PropertySources { + /** + * @see PropertySourceChain#or(PropertySource) + */ + public static PropertySourceChain from(PropertySource source) { + return from(source, null); + } + + /** + * @see PropertySourceChain#or(PropertySource, Func1) + */ + public static PropertySourceChain from(PropertySource source, Func1 transformer) { + return new PropertySourceChainImpl(source, transformer); + } + + /** + * @see PropertySourceChain#orConstantsClass(Class) + */ + public static PropertySourceChain fromConstantsClass(Class constantsClass) { + return fromConstantsClass(constantsClass, null); + } + + /** + * @see PropertySourceChain#orConstantsClass(Class, Func1) + */ + public static PropertySourceChain fromConstantsClass(Class constantsClass, Func1 transformer) { + return from(new ConstantsClassSource<>(constantsClass), transformer); + } + + /** + * @see PropertySourceChain#orComputed(Func1) + */ + public static PropertySourceChain fromComputed(Func1 computation) { + return fromComputed(computation, null); + } + + /** + * @see PropertySourceChain#orComputed(Func1, Func1) + */ + public static PropertySourceChain fromComputed(Func1 computation, Func1 transformer) { + return from(new ComputedSource(computation), transformer); + } + + /** + * @see PropertySourceChain#orEnvironment() + */ + public static PropertySourceChain fromEnvironment() { + return fromEnvironment(null); + } + + /** + * @see PropertySourceChain#orEnvironment(Func1) + */ + public static PropertySourceChain fromEnvironment(Func1 transformer) { + return from(EnvironmentPropertiesSource.DEFAULT, transformer); + } + + public static PropertySourceChain fromFile(String path) { + return fromFile(path, null); + } + + /** + * @see PropertySourceChain#orFile(String) + */ + public static PropertySourceChain fromFile(String path, Func1 transformer) { + try { + return from(new FileSource(path), transformer); + } catch (IOException e) { + return from(EmptySource.INSTANCE); + } + } + + /** + * @see PropertySourceChain#orImmediate(Action1) + */ + public static PropertySourceChain fromImmediate(Action1 initializer) { + return fromImmediate(initializer, null); + } + + /** + * @see PropertySourceChain#orImmediate(Action1, Func1) + */ + public static PropertySourceChain fromImmediate(Action1 initializer, Func1 transformer) { + ImmediatePropertySourceImpl immediateSource = new ImmediatePropertySourceImpl(); + initializer.call(immediateSource); + + return from(immediateSource, transformer); + } + + /** + * @see PropertySourceChain#orSystem() + */ + public static PropertySourceChain fromSystem() { + return fromSystem(null); + } + + /** + * @see PropertySourceChain#orSystem(Func1) + */ + public static PropertySourceChain fromSystem(Func1 transformer) { + return from(SystemPropertiesSource.DEFAULT, transformer); + } + +} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/Qualifier.java b/core/src/main/java/foundation/stack/datamill/configuration/Qualifier.java new file mode 100644 index 0000000..b8f6e37 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/Qualifier.java @@ -0,0 +1,18 @@ +package foundation.stack.datamill.configuration; + +import java.lang.annotation.*; + +/** + * Annotation to be used to add a qualifier to constructor parameters, and to classes. Adding this annotation to a + * constructor means that a {@link QualifyingFactory} must have been + * + * @see Wiring + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER, ElementType.TYPE}) +@Repeatable(Qualifiers.class) +public @interface Qualifier { + String value(); +} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/Qualifiers.java b/core/src/main/java/foundation/stack/datamill/configuration/Qualifiers.java new file mode 100644 index 0000000..667b83e --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/Qualifiers.java @@ -0,0 +1,16 @@ +package foundation.stack.datamill.configuration; + +import java.lang.annotation.*; + +/** + * Annotation to be used to add qualifiers to constructor parameters, and to classes. + * + * @see Wiring + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER, ElementType.TYPE}) +public @interface Qualifiers { + Qualifier[] value(); +} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/QualifyingFactory.java b/core/src/main/java/foundation/stack/datamill/configuration/QualifyingFactory.java new file mode 100644 index 0000000..34f6ca3 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/QualifyingFactory.java @@ -0,0 +1,19 @@ +package foundation.stack.datamill.configuration; + +import rx.functions.Func3; + +import java.util.Collection; + +/** + * An object factory like {@link Factory} but one that requires qualifiers (see {@link Qualifier}) to be present before + * it constructs objects. The qualifying factory is passed a set of qualifiers (as a {@link Collection} of strings to + * allow the factory to test which qualifiers are present. + * + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +@FunctionalInterface +public interface QualifyingFactory extends Func3, Collection, R> { + static QualifyingFactory wrap(Factory factory) { + return (w, c, q) -> factory.call(w, (Class) c); + } +} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/Scope.java b/core/src/main/java/foundation/stack/datamill/configuration/Scope.java new file mode 100644 index 0000000..3b1454d --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/Scope.java @@ -0,0 +1,14 @@ +package foundation.stack.datamill.configuration; + +import java.util.Collection; + +/** + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public interface Scope { + R resolve( + Wiring wiring, + QualifyingFactory factory, + Class type, + Collection qualifiers); +} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/SingletonScope.java b/core/src/main/java/foundation/stack/datamill/configuration/SingletonScope.java new file mode 100644 index 0000000..bc01ae3 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/SingletonScope.java @@ -0,0 +1,29 @@ +package foundation.stack.datamill.configuration; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public class SingletonScope implements Scope { + private Map, Object> constructed = new HashMap<>(); + + @Override + public R resolve( + Wiring wiring, + QualifyingFactory factory, + Class type, + Collection qualifiers) { + Object instance = constructed.get(type); + if (instance == null) { + instance = factory.call(wiring, type, qualifiers); + if (instance != null) { + constructed.put(type, instance); + } + } + + return (R) instance; + } +} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/TypeLessFactory.java b/core/src/main/java/foundation/stack/datamill/configuration/TypeLessFactory.java new file mode 100644 index 0000000..6fdb2cd --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/TypeLessFactory.java @@ -0,0 +1,13 @@ +package foundation.stack.datamill.configuration; + +import rx.functions.Func1; + +/** + * An object factory like {@link Factory} but one that doesn't need the {@link Class} parameter to be passed into it in + * order to construct objects. + * + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +@FunctionalInterface +public interface TypeLessFactory extends Func1 { +} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/StringValue.java b/core/src/main/java/foundation/stack/datamill/configuration/Value.java similarity index 80% rename from core/src/main/java/foundation/stack/datamill/configuration/StringValue.java rename to core/src/main/java/foundation/stack/datamill/configuration/Value.java index 26c4b7d..b550c52 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/StringValue.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/Value.java @@ -6,7 +6,7 @@ * 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"; + * \@Value("value") public static final String PROPERTY_NAME = "configuration/propertyName"; * * This defines a property called "configuration/propertyName" that has the value "value". * @@ -15,6 +15,6 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD}) -public @interface StringValue { +public @interface Value { 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 8d32da3..5ca4cad 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/Wiring.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/Wiring.java @@ -1,23 +1,13 @@ package foundation.stack.datamill.configuration; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; -import foundation.stack.datamill.Pair; -import foundation.stack.datamill.configuration.impl.Classes; -import foundation.stack.datamill.reflection.impl.TypeSwitch; import foundation.stack.datamill.values.StringValue; import foundation.stack.datamill.values.Value; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import rx.functions.Action1; import rx.functions.Func1; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Modifier; -import java.lang.reflect.Parameter; -import java.text.MessageFormat; -import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; /** *

@@ -28,37 +18,59 @@ * For example, consider: *

  * public class UserRepository {
- *     public UserRepository(DatabaseClient databaseClient, OutlineBuilder outlineBuilder) {
+ *     public UserRepository(DataStore dataStore) {
  *     }
  * }
  * 
*

*

- * You can use a Wiring to construct a UserRepository: + * You can use a Wiring to create a UserRepository instance: *

  * UserRepository repository = new Wiring()
- *     .add(new OutlineBuilder(), new DatabaseClient(...))
- *     .construct(UserRepository.class);
+ *     .newInstance(UserRepository.class);
  * 
*

*

- * This constructs a new UserRepository using the public constructor, injecting the provided instances of DatabaseClient - * and OutlineBuilder. Note that the wiring does not care about the ordering of the constructor parameters. + * This constructs a new UserRepository using the public constructor, creating an instance of DataStore, and any + * dependencies DataStore may have in the process. How did the wiring know how to construct DataStore and it's + * dependencies? By default, a Wiring will automatically create instances of concrete classes for which it can + * transitively create dependencies. But what if we have an interface or a class that doesn't have dependencies that + * can be satisfied? You setup the Wiring with a {@link FactoryChain} that tells it how to construct specific classes. + * For example, consider: *

+ *
+ * public class ApplicationRepository {
+ *     public ApplicationRepository(DatabaseClient databaseClient, OutlineBuilder outlineBuilder) {
+ *     }
+ * }
+ * 
+ *
+ * FactoryChain chain = FactoryChains.forType(DatabaseClient.class, w -> new DatabaseClient(...))
+ *         .thenForType(OutlineBuilder.class, w -> OutlineBuilder.DEFAULT)
+ *         .thenForAnyConcreteClass();
+ * ApplicationRepository repository = new Wiring(chain).newInstance(ApplicationRepository.class);
+ * 
*

- * Factory methods can be registered with the Wiring using {@link #addFactory(Class, Func1)} and the Wiring will use the - * factory methods whenever it cannot satisfy dependencies with explicitly added and constructed instances. Note that - * once a factory method is invoked to construct an instance for a type, the built instance will be added to the Wiring. + * Note that the {@link FactoryChain} we built and passed in to the Wiring will tell it exactly how to build objects of + * the types it encounters. The chain will be consulted in the order it is composed - in this case, first, if the + * Wiring needs to construct a DatabaseClient, it will use the first lambda provided. If it needs an OutlineBuilder, it + * will use the second lambda and for all other concrete classes, it will attempt to auto-construct them. It will use + * the chain every time a dependency needs to be constructed. Use {@link FactoryChains} as a starting point for creating + * factory chains. *

*

- * When the constructor's parameter types are concrete classes, Wirings will attempt to recursively construct instances - * of those concrete types. Most of the time, this is the intended behavior, and it reduces the amount of explicit calls - * that are required. Note that the Wiring will first attempt to use explicitly added and constructed instances when - * trying to satisfy dependencies before constructing them automatically. + * Notice that in the previous examples we called the newInstance method on the Wiring. This will create a new instance + * of that class every single time. While this is sometimes the desired behavior, most of the time, you will want to + * use {@link Wiring#singleton(Class)} instead to create a singleton instance of a type. If you call singleton to + * construct instances of some type multiple times, you will get back the same singleton instance every time. The + * singletons are scoped to the {@link Wiring}'s default singleton {@link Scope}. A different {@link Scope} can be + * passed in to a Wiring when it is constructed to change how instances are scoped. If you pass in a different scope, + * you can use the {@link Wiring#defaultScoped(Class)} method to construct instances using that scope. *

*

- * When dealing with a constructor which has multiple parameters of the same type, Wirings support using a name as a - * qualifier for constructor injection. For example, consider: + * In addition to using the factory chain to create dependencies, a Wiring will also resolve any {@link Named} + * constructor parameters using a {@link PropertySource} it is setup with. This is useful for picking up externalized + * properties from constants classes, files, environment variables and system properties. For example, consider: *

  * public class DatabaseClient {
  *     public DatabaseClient(@Named("url") String url, @Named("username") String username, @Named("password") String password) {
@@ -69,655 +81,135 @@
  * 

* You can use a Wiring to construct a DatabaseClient using the named parameters: *

- * DatabaseClient client = new Wiring()
- *     .addFormatted("url", "jdbc:mysql://{0}:{1}/{2}", "localhost", 3306, "database")
- *     .addNamed("username", "dbuser")
- *     .addNamed("password", "dbpass")
- *     .construct(DatabaseClient.class);
+ * DatabaseClient client = new Wiring(
+ *     PropertySources.fromFile("database.properties")
+ *         .orImmediate(s -> s
+ *             .put("url", "jdbc:mysql://localhost:3306")
+ *             .put("username", "dbuser")
+ *             .put("password", "dbpass")))
+ *     .singleton(DatabaseClient.class);
  * 
*

*

- * This constructs a new DatabaseClient using the constructor shown, injecting the provided named Strings as parameters. - * Note that for Strings and primitives, Wirings only support injection using a name. - *

- *

- * Wirings are very light-weight containers for objects and properties that are meant to be wired together. Each - * separate Wiring instance is self-contained, and when the {@link #construct(Class)} method is called, only - * the objects (including named objects) added to the Wiring are considered as candidates when injecting. - *

- *

- * You can associate a {@link PropertySource} or {@link PropertySourceChain} with a Wiring using - * {@link #setNamedPropertySource} so that it can resolve any named parameters by looking them up in the - * {@link PropertySource}. Note that any named values explicitly added to the Wiring using - * {@link #addNamed(String, Object)} will take precedence over the property source. + * This constructs a new DatabaseClient using the constructor shown, injecting the provided named values as parameters. + * As with factories, property sources are setup as a chain - you create {@link PropertySourceChain}s using + * {@link PropertySources} as a starting point. You can see how in this example, the properties will first be looked up + * in a "database.properties" file, and will use some defaults as a fallback. Note that only Strings, primitives and + * primitive wrappers are supported for {@link Named} parameters. *

* * @author Ravi Chodavarapu (rchodava@gmail.com) */ public class Wiring { - private static final Logger logger = LoggerFactory.getLogger(Wiring.class); - - private static final TypeSwitch valueCast = new TypeSwitch() { - @Override - protected Object caseBoolean(Value value, Void __) { - return value.asBoolean(); - } - - @Override - protected Object caseByte(Value value, Void __) { - return value.asByte(); - } - - @Override - protected Object caseCharacter(Value value, Void __) { - return value.asCharacter(); - } - - @Override - protected Object caseShort(Value value, Void __) { - return value.asShort(); - } - - @Override - protected Object caseInteger(Value value, Void __) { - return value.asInteger(); - } - - @Override - protected Object caseLong(Value value, Void __) { - return value.asLong(); - } - - @Override - protected Object caseFloat(Value value, Void __) { - return value.asFloat(); - } - - @Override - protected Object caseDouble(Value value, Void __) { - return value.asDouble(); - } - - @Override - protected Object caseLocalDateTime(Value value, Void __) { - return value.asLocalDateTime(); - } - - @Override - protected Object caseByteArray(Value value, Void __) { - return value.asByteArray(); - } - - @Override - protected Object caseString(Value value1, Void value2) { - return value1.asString(); - } - - @Override - protected Object defaultCase(Value value, Void __) { - return value; - } - }; - - private static boolean canAutoConstruct(Class type) { - return !type.isInterface() && - !Modifier.isAbstract(type.getModifiers()) && - type != String.class && - !type.isPrimitive() && - !Classes.isPrimitiveWrapper(type); - } - - private final Map, List>>> factories = new HashMap<>(); - private final Multimap, Object> members = HashMultimap.create(); - private final Map named = new HashMap<>(); - private PropertySource propertySource; - - /** - * Add the specified modules to this wiring. - * - * @param modules Modules to add. - */ - public Wiring include(Module... modules) { - for (Module module : modules) { - module.call(this); - } - return this; - } - - private void add(Class clazz, Object addition) { - members.put(clazz, addition); - - registerUnderParentClass(clazz, addition); - registerUnderInterfaces(clazz, addition); - } - - private void registerUnderInterfaces(Class clazz, Object addition) { - Class[] interfaces = clazz.getInterfaces(); - if (interfaces != null) { - for (Class interfaceClass : interfaces) { - add(interfaceClass, addition); - } - } - } - - private void registerUnderParentClass(Class clazz, Object addition) { - Class superClass = clazz.getSuperclass(); - if (superClass != null && superClass != Object.class) { - add(superClass, addition); - } - } - - /** - * Add one or more objects to the Wiring. These objects are then available for constructor injection when any - * matching constructor parameters are found. Adding primitives to a Wiring without a name (i.e., without using - * {@link #addNamed(String, Object)} is not supported). - * - * @param additions Objects to add. - */ - public Wiring add(Object... additions) { - if (additions == null) { - throw new IllegalArgumentException("Cannot add null to a Wiring"); - } - - for (Object addition : additions) { - if (addition == null) { - throw new IllegalArgumentException("Cannot add null to a Wiring"); - } - - if (addition instanceof String || addition.getClass().isPrimitive()) { - throw new IllegalArgumentException("Cannot add Strings and primitives to a Wiring without a name"); - } - - if (addition instanceof Optional) { - add(((Optional) addition).orElse(null)); - } else { - add(addition.getClass(), addition); - } - } - - return this; - } - - /** - * Add a factory method to the Wiring which is invoked to create an instance of the specified class. The factory - * method is added with a default priority of 0. - * - * @param clazz Class for which we want to add a factory. - * @param factory Factory method to invoke for the class specified. - */ - public Wiring addFactory(Class clazz, Func1 factory) { - return addFactory(0, clazz, factory); - } - - /** - * Add a factory method to the Wiring which is invoked to create an instance of the specified class. - * - * @param priority Priority to give this factory - if multiple factory methods exist for a type, the instance - * returned by the method with higher priority will be used. - * @param clazz Class for which we want to add a factory. - * @param factory Factory method to invoke for the class specified. - */ - public Wiring addFactory(int priority, Class clazz, Func1 factory) { - factories.compute(clazz, (__, existing) -> { - if (existing == null) { - existing = new ArrayList<>(); - } - - int insertionPoint = 0; - for (int i = 0; i < existing.size(); i++) { - if (priority >= existing.get(i).getFirst()) { - insertionPoint = i; - break; - } - } - existing.add(insertionPoint, new Pair<>(priority, factory)); - return existing; - }); - return this; - } - - /** - * Add an object to the Wiring under the specified name. These objects are only injected when a constructor - * parameter has a {@link Named} annotation with the specified name. - * - * @param name Name for the object. - * @param addition Object to add. - */ - public Wiring addNamed(String name, Object addition) { - if (addition == null) { - throw new IllegalArgumentException("Cannot add null to graph"); - } + private final PropertySource propertySource; + private final QualifyingFactory factory; + private final Scope defaultScope; + private final Scope singletonScope; - if (addition instanceof Optional) { - addition = ((Optional) addition).orElse(null); - return addNamed(name, addition); - } - - if (named.containsKey(name)) { - throw new IllegalArgumentException("Wiring already contains an object with name " + name); - } - - named.put(name, addition); - return this; + public Wiring(PropertySource propertySource, QualifyingFactory factory, Scope defaultScope) { + this.propertySource = propertySource; + this.singletonScope = new SingletonScope(); + this.defaultScope = defaultScope != null ? defaultScope : singletonScope; + this.factory = factory; } - /** - * Add a new formatted string to the Wiring under the specified name. These strings are only injected when a constuctor - * parameter has a {@link Named} annotation with the specified name. - * - * @param name Name for the string. - * @param format Format template to use for the string. - * @param arguments Arguments to be used with the template to construct a formatted string. - */ - public Wiring addFormatted(String name, String format, Object... arguments) { - Object[] casted = new Object[arguments.length]; - for (int i = 0; i < arguments.length; i++) { - if (arguments[i] instanceof Value) { - casted[i] = ((Value) arguments[i]).asString(); - } else { - casted[i] = arguments[i]; - } - } - - addNamed(name, MessageFormat.format(format, casted)); - return this; + public Wiring(PropertySource propertySource, QualifyingFactory factory) { + this(propertySource, factory, null); } - /** - * Construct an instance of the specified class using one of it's public constructors. This method will use the first - * constructor for which it can provide all parameters. The Wiring will use all the objects that it currently knows - * about (i.e., all objects that have been added or constructed by this Wiring at the time the construct method is - * called) to perform the injection. After the instance is constructed, the instance is added to the Wiring as one - * of the objects it knows about for injection into other constructors. If a particular dependency is unsatisfied, - * if the parameter's type is a concrete class, the Wiring will try to recursively {@link #construct(Class)} an - * instance of that parameter type. - * - * @param clazz Class we want to create an instance of. - * @param Type of instance. - * @return Instance that was constructed. - * @throws IllegalArgumentException If the class is an interface, abstract class or has no public constructors. - * @throws IllegalStateException If all dependencies for constructing an instance cannot be satisfied. - */ - public T construct(Class clazz) { - return construct(clazz, new HashSet<>()); + public Wiring(QualifyingFactory factory, Scope defaultScope) { + this(null, factory, defaultScope); } - private T construct(Class clazz, Set> autoConstructionCandidates) { - autoConstructionCandidates.add(clazz); - Constructor[] constructors = getPublicConstructors(clazz); - - for (Constructor constructor : constructors) { - T instance = constructWithConstructor(clazz, constructor, autoConstructionCandidates); - if (instance != null) { - return instance; - } - } - - throw new IllegalStateException("Unable to satisfy all dependencies needed to construct instance of " + clazz.getName()); + public Wiring(QualifyingFactory factory) { + this(factory, null); } - /** - * Construct an instance of the specified class using a public constructors that has parameters of the specified - * types. - * - * @see #construct(Class) - */ - public T constructWith(Class clazz, Class... parameterTypes) { - try { - Constructor constructor = clazz.getConstructor(parameterTypes); - return constructWithConstructor(clazz, constructor, new HashSet<>()); - } catch (NoSuchMethodException | SecurityException e) { - throw new IllegalStateException("Unable to find a constructor with specified parameters on " + clazz.getName()); - } + public Wiring(PropertySource propertySource, Scope defaultScope) { + this(propertySource, FactoryChains.forAnyConcreteClass(), defaultScope); } - private T constructWithConstructor( - Class clazz, - Constructor constructor, - Set> autoConstructionCandidates) { - Parameter[] parameters = constructor.getParameters(); - Object[] values = new Object[parameters.length]; - - for (int i = 0; i < parameters.length; i++) { - values[i] = getValueForParameter(parameters[i]); - if (values[i] == null) { - Class parameterType = parameters[i].getType(); - if (canAutoConstruct(parameterType) && !autoConstructionCandidates.contains(parameterType)) { - try { - values[i] = construct(parameterType, autoConstructionCandidates); - } catch (IllegalArgumentException | IllegalStateException e) { - logger.error("Could not build class {} as the following type could not be constructed {}", clazz, parameterType, e); - return null; - } - } else { - 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; - } - } - } - - return instantiate(clazz, constructor, values); + public Wiring(PropertySource propertySource) { + this(propertySource, (Scope) null); } - public T instantiate(Class clazz, Constructor constructor, Object[] arguments) { - try { - T constructed = (T) constructor.newInstance(arguments); - add(constructed); - return constructed; - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new IllegalArgumentException("Unable to construct instance of " + clazz.getName(), e); - } + public Wiring() { + this((PropertySource) null); } - private Constructor[] getPublicConstructors(Class clazz) { - Constructor[] constructors = clazz.getConstructors(); - if (constructors == null || constructors.length == 0) { - throw new IllegalArgumentException("Class " + clazz.getName() + " has no public constructors!"); - } - - return constructors; + private Wiring(PropertySource propertySource, QualifyingFactory factory, Scope defaultScope, Scope singletonScope) { + this.propertySource = propertySource; + this.singletonScope = singletonScope; + this.defaultScope = defaultScope; + this.factory = factory; } - private Object getValueForParameter(Parameter parameter) { - Named[] names = parameter.getAnnotationsByType(Named.class); - if (names != null && names.length > 0) { - Object namedValue = getValueForNamedParameter(parameter, names); - if (namedValue != null) { - return namedValue; - } - } else { - Object value = getValueForParameterByType(parameter); - if (value != null) { - return value; - } - } - - - return null; + public Value getRequiredNamed(String property) { + return getNamed(property) + .orElseThrow(() -> new IllegalStateException("Required named property [" + property + "] not present!")); } - private Object getValueForParameterByType(Parameter parameter) { - Class type = parameter.getType(); - return get(type); + public Optional getNamed(String property) { + return propertySource != null ? + propertySource.get(property).map(value -> new StringValue(value)) : + Optional.empty(); } - /** - * Get an object of the specified type that was previously added to or constructed by this wiring, or that can be - * built by a registered factory, or null if no value of the specified type was added or constructed. - */ - public T get(Class type) { - if (type.isPrimitive() || type == String.class || Classes.isPrimitiveWrapper(type)) { - logger.debug("Injection of dependencies that primitives and not named is not supported!"); - return null; - } - - Object value = getObjectOfType(type); - if (value != null) { - return (T) value; - } - - value = getValueOfType(type); - if (value != null) { - return (T) value; - } - - List>> typeFactories = factories.get(type); - if (typeFactories != null) { - for (Pair> factory : typeFactories) { - value = factory.getSecond().call(this); - if (value != null) { - T casted = (T) value; - add(casted); - return casted; - } - } - } - - return null; + private R construct(Scope scope, Class type, Collection qualifiers) { + return scope != null ? + (R) scope.resolve(this, factory, (Class) type, qualifiers) : + (R) factory.call(this, (Class) type, qualifiers); } - /** - * @see #getNamedAs(String, Class) - */ - public Value getNamed(String name) { - return getNamedAs(name, Value.class); + public R defaultScoped(Class type) { + return (R) defaultScoped(type, Collections.emptyList()); } - /** - * Get the object identified by the give name that was added by using {@link Wiring#addNamed(String, Object)}, - * or available from the property source chain set on the wiring. - */ - public T getNamedAs(String name, Class type) { - Object value = named.get(name); - - if (value == null && propertySource != null) { - value = propertySource.get(name).map(string -> new StringValue(string)).orElse(null); - } - - if (value != null) { - if (type.isInstance(value)) { - return (T) value; - } - - if (type.isPrimitive()) { - Class wrapper = Classes.primitiveToWrapper(type); - if (wrapper.isInstance(value)) { - return (T) value; - } - } - - if (Value.class.isAssignableFrom(value.getClass())) { - if (type == Value.class) { - return (T) value; - } - - return (T) castValueToTypeIfPossible((Value) value, type); - } - - if (type == Value.class) { - return (T) new StringValue(value.toString()); - } - } - - return null; + public R defaultScoped(Class type, String... qualifiers) { + return (R) defaultScoped(type, Arrays.asList(qualifiers)); } - private Object getObjectOfType(Class type) { - Collection values = members.get(type); - if (values != null) { - if (values.size() == 0) { - return null; - } - - if (values.size() == 1) { - return values.iterator().next(); - } - - throw new IllegalStateException("Multiple objects in graph match type " + type.getName()); - } - - return null; + public R defaultScoped(Class type, Collection qualifiers) { + return (R) construct(defaultScope, type, qualifiers); } - private Object getValueOfType(Class type) { - Collection values = members.get(Value.class); - if (values != null) { - if (values.size() == 0) { - return null; - } - - ArrayList casted = new ArrayList<>(); - for (Object value : values) { - Object castedValue = castValueToTypeIfPossible((Value) value, type); - if (castedValue != null) { - casted.add(castedValue); - } - } - - if (casted.size() == 1) { - return casted.iterator().next(); - } - - if (casted.size() == 0) { - return null; - } - - throw new IllegalStateException("Multiple objects in graph match type " + type.getName()); - } - - return null; + public R newInstance(Class type) { + return newInstance(type, Collections.emptyList()); } - private Object getValueForNamedParameter(Parameter parameter, Named[] names) { - for (Named name : names) { - Object value = getValueForNamedParameter(parameter, name); - if (value != null) { - return value; - } - } - - return null; + public R newInstance(Class type, String... qualifiers) { + return newInstance(type, Arrays.asList(qualifiers)); } - private Object getValueForNamedParameter(Parameter parameter, Named name) { - return getNamedAs(name.value(), parameter.getType()); + public R newInstance(Class type, Collection qualifiers) { + return construct(null, type, qualifiers); } - - private Object castValueToTypeIfPossible(Value value, Class type) { - Object castedValue = valueCast.doSwitch(type, value, null); - if (castedValue != null && Classes.isAssignable(type, castedValue.getClass())) { - return castedValue; - } - - return null; + public R singleton(Class type) { + return singleton(type, Collections.emptyList()); } - /** - * Similar convenience mechanism to {@link #with(Action1)} but the action is invoked if condition is true. If it is - * not true, an else clause can be chained, as in: - *

- *

-     * new Wiring().add(...)
-     *     .performIf(..., w -> {
-     *         w.construct(...);
-     *     })
-     *     .orElse(...);
-     * 
- */ - public ElseBuilder performIf(boolean condition, Action1 consumer) { - if (condition) { - consumer.call(this); - } - - return new ElseBuilder() { - @Override - public Wiring orElseDoNothing() { - return Wiring.this; - } - - @Override - public Wiring orElse(Action1 consumer) { - if (!condition) { - consumer.call(Wiring.this); - } - - return Wiring.this; - } - }; + public R singleton(Class type, String... qualifiers) { + return singleton(type, Arrays.asList(qualifiers)); } - /** - * Similar convenience mechanism to {@link #with(Action1)} and {@link #performIf(boolean, Action1)} but the function - * is invoked if condition is true. If it is not true, an else clause can be chained, as in: - *

- *

-     * new Wiring().add(...)
-     *     .returnIf(..., w -> {
-     *         w.construct(...);
-     *     })
-     *     .orElse(...);
-     * 
- */ - public ReturningElseBuilder returnIf(boolean condition, Func1 function) { - if (condition) { - return new ReturningElseBuilder() { - @Override - public T orElseReturnNull() { - return function.call(Wiring.this); - } - - @Override - public T orElse(Func1 __) { - return function.call(Wiring.this); - } - }; - } else { - return new ReturningElseBuilder() { - @Override - public T orElseReturnNull() { - return null; - } - - @Override - public T orElse(Func1 elseFunction) { - return elseFunction.call(Wiring.this); - } - }; - } + public R singleton(Class type, Collection qualifiers) { + return construct(singletonScope, type, qualifiers); } - public Wiring setNamedPropertySource(PropertySource propertySource) { - this.propertySource = propertySource; - return this; + public R with(PropertySource propertySource, QualifyingFactory factory, Func1 wiringConsumer) { + return wiringConsumer.call(new Wiring(propertySource, factory, defaultScope, singletonScope)); } - /** - * A convenience mechanism to quickly construct multiple objects - for example: - *
-     * new Wiring().add(databaseClient, outlineBuilder).with(w -> {
-     *     w.construct(UserRepository.class);
-     *     w.construct(WidgetRepository.class);
-     *
-     *     w.construct(UserController.class);
-     *     w.construct(WidgetController.class);
-     * });
-     * 
- */ - public Wiring with(Action1 consumer) { - consumer.call(this); - return this; + public R with(PropertySource propertySource, Func1 wiringConsumer) { + return with(propertySource, factory, wiringConsumer); } - public interface ElseBuilder { - Wiring orElseDoNothing(); - - Wiring orElse(Action1 consumer); + public R with(QualifyingFactory factory, Func1 wiringConsumer) { + return with(propertySource, factory, wiringConsumer); } - public interface ReturningElseBuilder { - T orElseReturnNull(); - - T orElse(Func1 function); + public R with(Func1 wiringConsumer) { + return wiringConsumer.call(this); } } diff --git a/core/src/main/java/foundation/stack/datamill/configuration/WiringException.java b/core/src/main/java/foundation/stack/datamill/configuration/WiringException.java new file mode 100644 index 0000000..2fe68c5 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/WiringException.java @@ -0,0 +1,55 @@ +package foundation.stack.datamill.configuration; + +import java.util.List; + +/** + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public class WiringException extends RuntimeException { + private final List components; + + public WiringException(String message) { + this(message, null); + } + + public WiringException(String message, List components) { + super(message); + + this.components = components; + } + + public List getComponents() { + return components; + } + + private void indent(StringBuilder formatted, int indent) { + for (int i = 0; i < indent; i++) { + formatted.append(" "); + } + + formatted.append(" |- "); + } + + public String toString(int indent) { + StringBuilder formatted = new StringBuilder(); + + if (getMessage() != null) { + indent(formatted, indent); + formatted.append(getMessage()); + } + + if (components != null && components.size() > 0) { + for (WiringException component : components) { + formatted.append('\n'); + formatted.append(component.toString(indent + 1)); + } + } + + return formatted.toString(); + } + + @Override + public String toString() { + return toString(0); + } +} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/impl/AbstractSource.java b/core/src/main/java/foundation/stack/datamill/configuration/impl/AbstractSource.java index 2a049a5..d57efc3 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/impl/AbstractSource.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/AbstractSource.java @@ -3,7 +3,7 @@ import foundation.stack.datamill.configuration.PropertySource; import foundation.stack.datamill.values.StringValue; import foundation.stack.datamill.values.Value; -import rx.functions.Action1; +import rx.functions.Func1; import java.util.Optional; @@ -29,8 +29,7 @@ public Value getRequired(String name) { } @Override - public PropertySource with(Action1 propertiesConsumer) { - propertiesConsumer.call(this); - return this; + public R with(Func1 propertiesConsumer) { + return propertiesConsumer.call(this); } } diff --git a/core/src/main/java/foundation/stack/datamill/configuration/impl/Classes.java b/core/src/main/java/foundation/stack/datamill/configuration/impl/Classes.java index 9b0895a..e34809a 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/impl/Classes.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/Classes.java @@ -62,7 +62,7 @@ public static Class primitiveToWrapper(final Class clazz) { return convertedClass; } - private static Class wrapperToPrimitive(final Class clazz) { + public static Class wrapperToPrimitive(final Class clazz) { return wrapperPrimitiveMap.get(clazz); } diff --git a/core/src/main/java/foundation/stack/datamill/configuration/impl/ConcreteClassFactory.java b/core/src/main/java/foundation/stack/datamill/configuration/impl/ConcreteClassFactory.java new file mode 100644 index 0000000..60626fb --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/ConcreteClassFactory.java @@ -0,0 +1,166 @@ +package foundation.stack.datamill.configuration.impl; + +import foundation.stack.datamill.configuration.QualifyingFactory; +import foundation.stack.datamill.configuration.Wiring; +import foundation.stack.datamill.configuration.WiringException; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public class ConcreteClassFactory implements QualifyingFactory { + private static final ConcreteClassFactory INSTANCE = new ConcreteClassFactory<>(); + + private static Constructor[] getPublicConstructors(Class type) { + Constructor[] constructors = type.getConstructors(); + if (constructors == null || constructors.length == 0) { + throw new WiringException("Class [" + type.getName() + "] has no public constructors!"); + } + + return constructors; + } + + public static final ConcreteClassFactory instance() { + return INSTANCE; + } + + private static boolean canAutoConstruct(Class type) { + return !type.isInterface() && + !Modifier.isAbstract(type.getModifiers()) && + type != String.class && + !type.isPrimitive() && + !Classes.isPrimitiveWrapper(type); + } + + private static void performSecure(Runnable runnable) { + if (System.getSecurityManager() != null) { + AccessController.doPrivileged((PrivilegedAction) () -> { + runnable.run(); + return null; + }); + } else { + runnable.run(); + } + } + + private static void throwFailedConstructionException(Class type, List componentExceptions) { + StringBuilder message = new StringBuilder("Could not construct ["); + message.append(type.getName()); + message.append("] using any constructor!"); + throw new WiringException(message.toString(), Collections.unmodifiableList(componentExceptions)); + } + + private static void throwFailedInstantiationException(Constructor constructor, ReflectiveOperationException e) { + StringBuilder message = new StringBuilder("Failed to instantiate using constructor ["); + message.append(constructor.getName()); + message.append("] - "); + message.append(e.getMessage()); + throw new WiringException(message.toString()); + } + + private static void throwInvalidParameterValue(Parameter parameter, IllegalArgumentException e) { + StringBuilder message = new StringBuilder("Value retrieved for parameter ["); + message.append(parameter.getName()); + message.append("] was invalid: "); + message.append(e.getClass().getName()); + message.append(", "); + message.append(e.getMessage()); + + throw new WiringException(message.toString()); + } + + private static void throwUnsatisfiedParameter( + Constructor constructor, + Parameter parameter, + WiringException component) { + StringBuilder message = new StringBuilder("Failed to satisfy parameter ["); + message.append(parameter.getName()); + message.append(" ("); + message.append(parameter.getType().getName()); + message.append(")] in ["); + message.append(constructor.getName()); + message.append("]"); + + throw new WiringException(message.toString(), component != null ? Collections.singletonList(component) : null); + } + + private ConcreteClassFactory() { + } + + private R construct(Wiring wiring, Class type) { + Constructor[] constructors = getPublicConstructors(type); + ArrayList componentExceptions = new ArrayList<>(); + + for (Constructor constructor : constructors) { + try { + R instance = constructWithConstructor(wiring, constructor); + if (instance != null) { + return instance; + } + } catch (WiringException e) { + componentExceptions.add(e); + } + } + + throwFailedConstructionException(type, componentExceptions); + return null; + } + + private R constructWithConstructor( + Wiring wiring, + Constructor constructor) { + Parameter[] parameters = constructor.getParameters(); + Object[] values = new Object[parameters.length]; + + for (int i = 0; i < parameters.length; i++) { + Class parameterType = parameters[i].getType(); + try { + values[i] = wiring.defaultScoped(parameterType); + if (values[i] == null) { + try { + values[i] = NamedParameterValueRetriever.retrieveValueIfNamedParameter(wiring, parameters[i]); + } catch (IllegalArgumentException e) { + throwInvalidParameterValue(parameters[i], e); + } + } + } catch (WiringException e) { + throwUnsatisfiedParameter(constructor, parameters[i], e); + } + + if (values[i] == null) { + throwUnsatisfiedParameter(constructor, parameters[i], null); + } + } + + return instantiate(constructor, values); + } + + private R instantiate(Constructor constructor, Object[] arguments) { + try { + performSecure(() -> constructor.setAccessible(true)); + return (R) constructor.newInstance(arguments); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throwFailedInstantiationException(constructor, e); + return null; + } + } + + @Override + public R call(Wiring wiring, Class type, Collection qualifiers) { + if (canAutoConstruct(type)) { + return construct(wiring, type); + } + + 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 index 018ed69..c012cc1 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/impl/ConstantsClassSource.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/ConstantsClassSource.java @@ -1,8 +1,6 @@ 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 foundation.stack.datamill.configuration.Value; import javassist.Modifier; import java.lang.reflect.Field; @@ -54,19 +52,9 @@ private Map buildAnnotatedConstants() { 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())); - } - } + Value value = field.getAnnotation(Value.class); + if (value != null) { + constants.put(propertyName, value.value()); } } } diff --git a/core/src/main/java/foundation/stack/datamill/configuration/impl/FactoryChainImpl.java b/core/src/main/java/foundation/stack/datamill/configuration/impl/FactoryChainImpl.java new file mode 100644 index 0000000..552a469 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/FactoryChainImpl.java @@ -0,0 +1,124 @@ +package foundation.stack.datamill.configuration.impl; + +import foundation.stack.datamill.configuration.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; + +/** + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public class FactoryChainImpl implements FactoryChain { + public static Predicate> isExactType(Class factoryType) { + return type -> type == factoryType; + } + + public static Predicate> isSuperOf(Class factoryType) { + return type -> type.isAssignableFrom(factoryType); + } + + public static Predicate> tautology() { + return __ -> true; + } + + private final List delegates; + + public FactoryChainImpl(Predicate> condition, TypeLessFactory factory) { + this(condition, Factory.wrap(factory)); + } + + public FactoryChainImpl(Predicate> condition, Factory factory) { + this(condition, QualifyingFactory.wrap(factory)); + } + + public FactoryChainImpl(Predicate> condition, QualifyingFactory factory) { + this.delegates = Collections.singletonList(new FactoryDelegate(condition, factory)); + } + + private FactoryChainImpl(List delegates) { + this.delegates = delegates; + } + + @Override + public Object call(Wiring wiring, Class type, Collection qualifiers) { + for (FactoryDelegate delegate : delegates) { + if (delegate.condition.test(type)) { + Object constructed = delegate.factory.call(wiring, (Class) type, qualifiers); + if (constructed != null) { + return constructed; + } + } + } + + return null; + } + + private FactoryChain withAppendedDelegate(FactoryDelegate delegate) { + ArrayList delegates = new ArrayList<>(this.delegates); + delegates.add(delegate); + return new FactoryChainImpl(delegates); + } + + @Override + public FactoryChain thenForAny(Factory factory) { + return thenForAny(QualifyingFactory.wrap(factory)); + } + + @Override + public FactoryChain thenForAnyConcreteClass() { + return thenForAny(ConcreteClassFactory.instance()); + } + + @Override + public FactoryChain thenForSuperOf(Class type, Factory factory) { + return thenForSuperOf((Class) type, QualifyingFactory.wrap(factory)); + } + + @Override + public FactoryChain thenForType(Class type, Factory factory) { + return thenForType((Class) type, QualifyingFactory.wrap(factory)); + } + + @Override + public FactoryChain thenForAny(TypeLessFactory factory) { + return thenForAny(Factory.wrap(factory)); + } + + @Override + public FactoryChain thenForSuperOf(Class type, TypeLessFactory factory) { + return thenForSuperOf((Class) type, Factory.wrap(factory)); + } + + @Override + public FactoryChain thenForType(Class type, TypeLessFactory factory) { + return thenForType((Class) type, Factory.wrap(factory)); + } + + @Override + public FactoryChain thenForAny(QualifyingFactory factory) { + return withAppendedDelegate(new FactoryDelegate(tautology(), factory)); + } + + @Override + public FactoryChain thenForSuperOf(Class type, QualifyingFactory factory) { + return withAppendedDelegate(new FactoryDelegate(isSuperOf(type), factory)); + } + + @Override + public FactoryChain thenForType(Class type, QualifyingFactory factory) { + return withAppendedDelegate(new FactoryDelegate(isExactType(type), factory)); + } + + private static class FactoryDelegate { + private final Predicate> condition; + private final QualifyingFactory factory; + + public FactoryDelegate(Predicate> condition, QualifyingFactory factory) { + this.condition = condition; + this.factory = factory; + } + } +} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/impl/ImmediatePropertySourceImpl.java b/core/src/main/java/foundation/stack/datamill/configuration/impl/ImmediatePropertySourceImpl.java index a65df50..6fac8f9 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/impl/ImmediatePropertySourceImpl.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/ImmediatePropertySourceImpl.java @@ -1,7 +1,9 @@ package foundation.stack.datamill.configuration.impl; import foundation.stack.datamill.configuration.ImmediatePropertySource; +import foundation.stack.datamill.values.Value; +import java.text.MessageFormat; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -22,4 +24,19 @@ public ImmediatePropertySource put(String name, String value) { immediates.put(name, value); return this; } + + @Override + public ImmediatePropertySource put(String name, String format, Object... arguments) { + Object[] casted = new Object[arguments.length]; + for (int i = 0; i < arguments.length; i++) { + if (arguments[i] instanceof Value) { + casted[i] = ((Value) arguments[i]).asString(); + } else { + casted[i] = arguments[i]; + } + } + + return put(name, MessageFormat.format(format, casted)); + } + } diff --git a/core/src/main/java/foundation/stack/datamill/configuration/impl/NamedParameterValueRetriever.java b/core/src/main/java/foundation/stack/datamill/configuration/impl/NamedParameterValueRetriever.java new file mode 100644 index 0000000..c41c67d --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/NamedParameterValueRetriever.java @@ -0,0 +1,57 @@ +package foundation.stack.datamill.configuration.impl; + +import foundation.stack.datamill.configuration.Named; +import foundation.stack.datamill.configuration.Wiring; +import foundation.stack.datamill.configuration.WiringException; + +import java.lang.reflect.Parameter; + +/** + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public class NamedParameterValueRetriever { + private static Object getValueForNamedParameter(Wiring wiring, Parameter parameter, Named[] names) { + for (Named name : names) { + Object value = getValueForNamedParameter(wiring, parameter, name); + if (value != null) { + return value; + } + } + + throwUnsatisfiedNamedParameterException(names); + return null; + } + + private static Object getValueForNamedParameter(Wiring wiring, Parameter parameter, Named name) { + return SimpleValueConverter.convert(wiring.getNamed(name.value()).orElse(null), parameter.getType()); + } + + public static Object retrieveValueIfNamedParameter(Wiring wiring, Parameter parameter) { + Named[] names = parameter.getAnnotationsByType(Named.class); + if (names != null && names.length > 0) { + Object namedValue = getValueForNamedParameter(wiring, parameter, names); + if (namedValue != null) { + return namedValue; + } + } + + return null; + } + + private static void throwUnsatisfiedNamedParameterException(Named[] names) { + StringBuilder message = new StringBuilder("Failed to satisfy named parameter ["); + + for (int i = 0; i < names.length; i++) { + if (names[i].value() != null) { + message.append(names[i].value()); + if (i < names.length - 1) { + message.append(", "); + } + } + } + + message.append(']'); + + throw new WiringException(message.toString()); + } +} 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 new file mode 100644 index 0000000..3eb7723 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/PropertySourceChainImpl.java @@ -0,0 +1,130 @@ +package foundation.stack.datamill.configuration.impl; + +import foundation.stack.datamill.configuration.*; +import rx.functions.Action1; +import rx.functions.Func1; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public class PropertySourceChainImpl extends AbstractSource implements PropertySourceChain { + private static final Func1 IDENTITY = name -> name; + + private final List chain; + + public PropertySourceChainImpl(PropertySource initialSource, Func1 transformer) { + chain = Collections.singletonList(new TransformedSource(initialSource, transformer)); + } + + private PropertySourceChainImpl(List chain) { + this.chain = chain; + } + + @Override + public Optional getOptional(String name) { + for (TransformedSource source : chain) { + Optional value = source.propertySource.get(source.transformer.call(name)); + if (value.isPresent()) { + return value; + } + } + + return Optional.empty(); + } + + @Override + public PropertySourceChain or(PropertySource source) { + return or(source, null); + } + + @Override + public PropertySourceChain or(PropertySource source, Func1 transformer) { + ArrayList newChain = new ArrayList<>(chain); + newChain.add(new TransformedSource(source, transformer)); + + return new PropertySourceChainImpl(newChain); + } + + @Override + public PropertySourceChain orComputed(Func1 computation) { + return or(new ComputedSource(computation), null); + } + + @Override + public PropertySourceChain orComputed(Func1 computation, Func1 transformer) { + return or(new ComputedSource(computation), transformer); + } + + @Override + public PropertySourceChain orConstantsClass(Class constantsClass) { + return orConstantsClass(constantsClass, null); + } + + @Override + public PropertySourceChain orConstantsClass(Class constantsClass, Func1 transformer) { + return or(new ConstantsClassSource<>(constantsClass), transformer); + } + + @Override + public PropertySourceChain orEnvironment() { + return orEnvironment(null); + } + + @Override + public PropertySourceChain orEnvironment(Func1 transformer) { + return or(EnvironmentPropertiesSource.DEFAULT, transformer); + } + + @Override + public PropertySourceChain orFile(String path) { + return orFile(path, null); + } + + @Override + public PropertySourceChain orFile(String path, Func1 transformer) { + try { + return or(new FileSource(path), transformer); + } catch (IOException e) { + return or(EmptySource.INSTANCE); + } + } + + @Override + public PropertySourceChain orImmediate(Action1 initializer) { + return orImmediate(initializer, null); + } + + @Override + public PropertySourceChain orImmediate(Action1 initializer, Func1 transformer) { + ImmediatePropertySourceImpl defaults = new ImmediatePropertySourceImpl(); + initializer.call(defaults); + + return or(defaults, transformer); + } + + @Override + public PropertySourceChain orSystem() { + return orSystem(null); + } + + @Override + public PropertySourceChain orSystem(Func1 transformer) { + return or(SystemPropertiesSource.DEFAULT, transformer); + } + + private static class TransformedSource { + private final PropertySource propertySource; + private final Func1 transformer; + + public TransformedSource(PropertySource propertySource, Func1 transformer) { + this.propertySource = propertySource; + this.transformer = transformer != null ? transformer : IDENTITY; + } + } +} diff --git a/core/src/main/java/foundation/stack/datamill/configuration/impl/SimpleValueConverter.java b/core/src/main/java/foundation/stack/datamill/configuration/impl/SimpleValueConverter.java new file mode 100644 index 0000000..2c5de18 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/SimpleValueConverter.java @@ -0,0 +1,246 @@ +package foundation.stack.datamill.configuration.impl; + +import foundation.stack.datamill.reflection.impl.TypeSwitch; +import foundation.stack.datamill.values.StringValue; +import foundation.stack.datamill.values.Value; + +/** + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public class SimpleValueConverter { + private static final TypeSwitch downCast = new TypeSwitch() { + @Override + protected Object caseBoolean(Object value, Void __) { + return (boolean) value; + } + + @Override + protected Object caseByte(Object value, Void __) { + return (byte) (long) value; + } + + @Override + protected Object caseCharacter(Object value, Void __) { + return (char) (long) value; + } + + @Override + protected Object caseShort(Object value, Void __) { + return (short) (long) value; + } + + @Override + protected Object caseInteger(Object value, Void __) { + return (int) (long) value; + } + + @Override + protected Object caseLong(Object value, Void __) { + return (long) value; + } + + @Override + protected Object caseFloat(Object value, Void __) { + return (float) (double) value; + } + + @Override + protected Object caseDouble(Object value, Void __) { + return (double) value; + } + + @Override + protected Object caseLocalDateTime(Object value, Void __) { + return null; + } + + @Override + protected Object caseByteArray(Object value, Void __) { + return null; + } + + @Override + protected Object caseString(Object value1, Void value2) { + return null; + } + + @Override + protected Object defaultCase(Object value, Void __) { + return null; + } + }; + + private static final TypeSwitch upCast = new TypeSwitch() { + @Override + protected Object caseBoolean(Object value, Void __) { + return (boolean) value; + } + + @Override + protected Object caseByte(Object value, Void __) { + return (long) (byte) value; + } + + @Override + protected Object caseCharacter(Object value, Void __) { + return (long) (char) value; + } + + @Override + protected Object caseShort(Object value, Void __) { + return (long) (short) value; + } + + @Override + protected Object caseInteger(Object value, Void __) { + return (long) (int) value; + } + + @Override + protected Object caseLong(Object value, Void __) { + return (long) value; + } + + @Override + protected Object caseFloat(Object value, Void __) { + return (double) (float) value; + } + + @Override + protected Object caseDouble(Object value, Void __) { + return (double) value; + } + + @Override + protected Object caseLocalDateTime(Object value, Void __) { + return null; + } + + @Override + protected Object caseByteArray(Object value, Void __) { + return null; + } + + @Override + protected Object caseString(Object value1, Void value2) { + return null; + } + + @Override + protected Object defaultCase(Object value, Void __) { + return null; + } + }; + + private static final TypeSwitch valueCast = new TypeSwitch() { + @Override + protected Object caseBoolean(Value value, Void __) { + return value.asBoolean(); + } + + @Override + protected Object caseByte(Value value, Void __) { + return value.asByte(); + } + + @Override + protected Object caseCharacter(Value value, Void __) { + return value.asCharacter(); + } + + @Override + protected Object caseShort(Value value, Void __) { + return value.asShort(); + } + + @Override + protected Object caseInteger(Value value, Void __) { + return value.asInteger(); + } + + @Override + protected Object caseLong(Value value, Void __) { + return value.asLong(); + } + + @Override + protected Object caseFloat(Value value, Void __) { + return value.asFloat(); + } + + @Override + protected Object caseDouble(Value value, Void __) { + return value.asDouble(); + } + + @Override + protected Object caseLocalDateTime(Value value, Void __) { + return value.asLocalDateTime(); + } + + @Override + protected Object caseByteArray(Value value, Void __) { + return value.asByteArray(); + } + + @Override + protected Object caseString(Value value1, Void value2) { + return value1.asString(); + } + + @Override + protected Object defaultCase(Value value, Void __) { + return value; + } + }; + + private static Object castValueToTypeIfPossible(Value value, Class type) { + Object castedValue = valueCast.doSwitch(type, value, null); + if (castedValue != null && Classes.isAssignable(type, castedValue.getClass())) { + return castedValue; + } + + return null; + } + + public static T convert(Object value, Class type) { + if (value != null) { + if (type.isInstance(value)) { + return (T) value; + } + + if (type.isPrimitive()) { + Class wrapper = Classes.primitiveToWrapper(type); + if (wrapper.isInstance(value.getClass())) { + return (T) value; + } + + Class valuePrimitive = Classes.wrapperToPrimitive(value.getClass()); + if (Classes.isAssignable(valuePrimitive, type) || Classes.isAssignable(type, valuePrimitive)) { + return (T) downCast.doSwitch(type, upCast.doSwitch(value.getClass(), value, null), null); + } + } else if (Classes.isPrimitiveWrapper(type)) { + Class typePrimitive = Classes.wrapperToPrimitive(type); + Class valuePrimitive = Classes.wrapperToPrimitive(value.getClass()); + + if (Classes.isAssignable(valuePrimitive, typePrimitive) || Classes.isAssignable(typePrimitive, valuePrimitive)) { + return (T) downCast.doSwitch(typePrimitive, upCast.doSwitch(value.getClass(), value, null), null); + } + } + + if (Value.class.isAssignableFrom(value.getClass())) { + if (type == Value.class) { + return (T) value; + } + + return (T) castValueToTypeIfPossible((Value) value, type); + } + + if (type == Value.class) { + return (T) new StringValue(value.toString()); + } + } + + return null; + } +} diff --git a/core/src/test/java/foundation/stack/datamill/configuration/FactoryChainsTest.java b/core/src/test/java/foundation/stack/datamill/configuration/FactoryChainsTest.java new file mode 100644 index 0000000..3993ac6 --- /dev/null +++ b/core/src/test/java/foundation/stack/datamill/configuration/FactoryChainsTest.java @@ -0,0 +1,115 @@ +package foundation.stack.datamill.configuration; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public class FactoryChainsTest { + @Test + public void chains() { + ConcreteClass instance = new ConcreteClass(); + DerivedClass derived = new DerivedClass(); + + assertEquals(instance, + new Wiring(FactoryChains.forType(ConcreteClass.class, w -> instance).thenForAnyConcreteClass()) + .singleton(ConcreteClass.class)); + + assertEquals(derived, + new Wiring(FactoryChains.forSuperOf(DerivedClass.class, w -> derived).thenForAnyConcreteClass()) + .singleton(ConcreteClass.class)); + + assertEquals(derived, + new Wiring(FactoryChains.forSuperOf(DerivedClass.class, w -> derived).thenForAnyConcreteClass()) + .singleton(Interface.class)); + + assertEquals(derived, + new Wiring(FactoryChains.forSuperOf(DerivedClass.class, w -> derived).thenForAnyConcreteClass()) + .singleton(DerivedClass.class)); + + assertEquals(instance, + new Wiring(FactoryChains.forAny(w -> instance).thenForAnyConcreteClass()) + .singleton(ConcreteClass.class)); + + assertTrue(new Wiring(FactoryChains.forAnyConcreteClass().thenForSuperOf(DerivedClass.class, w -> derived)) + .singleton(ConcreteClass.class) instanceof ConcreteClass); + + assertTrue(new Wiring(FactoryChains.forAnyConcreteClass().thenForSuperOf(DerivedClass.class, w -> derived)) + .singleton(ConcreteClass.class) != derived); + + assertTrue(new Wiring(FactoryChains.forAnyConcreteClass().thenForType(DerivedClass.class, w -> derived)) + .singleton(DerivedClass.class) != derived); + } + + @Test + public void typedFactories() { + ConcreteClass instance = new ConcreteClass(); + DerivedClass derived = new DerivedClass(); + + assertEquals(instance, + new Wiring(FactoryChains.forType(ConcreteClass.class, (w, c) -> c == ConcreteClass.class ? instance : null) + .thenForAnyConcreteClass()) + .singleton(ConcreteClass.class)); + + assertEquals(derived, + new Wiring(FactoryChains.forSuperOf(DerivedClass.class, (w, c) -> c == (Class) ConcreteClass.class ? derived : null) + .thenForAnyConcreteClass()) + .singleton(ConcreteClass.class)); + + assertEquals(instance, + new Wiring(FactoryChains.forAny((w, c) -> c == ConcreteClass.class ? instance : null) + .thenForAnyConcreteClass()) + .singleton(ConcreteClass.class)); + } + + @Test + public void qualifiedFactories() { + ConcreteClass instance = new ConcreteClass(); + DerivedClass derived = new DerivedClass(); + + assertTrue(instance != + new Wiring(FactoryChains.forType(ConcreteClass.class, + (w, c, q) -> q.contains("qualifier") ? instance : null) + .thenForAnyConcreteClass()) + .singleton(ConcreteClass.class)); + + assertEquals(instance, + new Wiring(FactoryChains.forType(ConcreteClass.class, + (w, c, q) -> q.contains("qualifier") ? instance : null) + .thenForAnyConcreteClass()) + .singleton(ConcreteClass.class, "qualifier")); + + assertTrue(derived != + new Wiring(FactoryChains.forSuperOf(DerivedClass.class, + (w, c, q) -> q.contains("qualifier") ? derived : null) + .thenForAnyConcreteClass()) + .singleton(ConcreteClass.class)); + + assertEquals(instance, + new Wiring(FactoryChains.forType(ConcreteClass.class, + (w, c, q) -> q.contains("qualifier") ? instance : null) + .thenForAnyConcreteClass()) + .singleton(ConcreteClass.class, "qualifier")); + + assertEquals(instance, + new Wiring(FactoryChains.forAny((w, c, q) -> q.contains("qualifier") ? instance : null) + .thenForAnyConcreteClass()) + .singleton(ConcreteClass.class, "qualifier")); + } + + private interface Interface { + } + + private static class ConcreteClass { + public ConcreteClass() { + } + } + + private static class DerivedClass extends ConcreteClass implements Interface { + public DerivedClass() { + } + } +} diff --git a/core/src/test/java/foundation/stack/datamill/configuration/PropertySourceChainTest.java b/core/src/test/java/foundation/stack/datamill/configuration/PropertySourceChainTest.java deleted file mode 100644 index 06d5218..0000000 --- a/core/src/test/java/foundation/stack/datamill/configuration/PropertySourceChainTest.java +++ /dev/null @@ -1,237 +0,0 @@ -package foundation.stack.datamill.configuration; - -import com.google.common.base.Charsets; -import com.google.common.io.Files; -import org.junit.Rule; -import org.junit.Test; -import org.junit.contrib.java.lang.system.EnvironmentVariables; - -import java.io.File; -import java.io.IOException; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.fail; - -/** - * @author Ravi Chodavarapu (rchodava@gmail.com) - */ -public class PropertySourceChainTest { - @Rule - public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); - - @Test - public void environmentVariables() { - environmentVariables.set("test", "value"); - environmentVariables.set("prefix_transformed", "value2"); - - assertEquals("value", PropertySourceChain.ofEnvironment().get("test").get()); - assertFalse(PropertySourceChain.ofEnvironment().get("test2").isPresent()); - - assertFalse(PropertySourceChain.ofEnvironment().get("transformed").isPresent()); - assertEquals("value2", PropertySourceChain.ofEnvironment(v -> "prefix_" + v).get("transformed").get()); - - assertEquals("value", PropertySourceChain.ofEnvironment().getRequired("test").asString()); - } - - @Test - public void systemProperties() { - System.setProperty("test", "value"); - System.setProperty("prefix_transformed", "value2"); - - assertEquals("value", PropertySourceChain.ofSystem().get("test").get()); - assertFalse(PropertySourceChain.ofSystem().get("test2").isPresent()); - - assertFalse(PropertySourceChain.ofSystem().get("transformed").isPresent()); - assertEquals("value2", PropertySourceChain.ofSystem(v -> "prefix_" + v).get("transformed").get()); - - assertEquals("value", PropertySourceChain.ofSystem().getRequired("test").asString()); - } - - @Test - public void constantClasses() { - assertEquals("value", PropertySourceChain.ofConstantsClass(ConstantsClass.class).get(ConstantsClass.property).get()); - assertFalse(PropertySourceChain.ofConstantsClass(ConstantsClass.class).get("property2").isPresent()); - assertFalse(PropertySourceChain.ofConstantsClass(ConstantsClass.class).get("config/instance").isPresent()); - - assertEquals("publicValue", PropertySourceChain.ofConstantsClass(ConstantsClass.class).get(ConstantsClass.publicProperty).get()); - assertEquals("privateValue", PropertySourceChain.ofConstantsClass(ConstantsClass.class).get(ConstantsClass.privateProperty).get()); - assertEquals("nonFinalValue", PropertySourceChain.ofConstantsClass(ConstantsClass.class).get(ConstantsClass.nonFinalProperty).get()); - assertEquals("1", PropertySourceChain.ofConstantsClass(ConstantsClass.class).get(ConstantsClass.integerProperty).get()); - assertEquals("true", PropertySourceChain.ofConstantsClass(ConstantsClass.class).get(ConstantsClass.booleanProperty).get()); - - assertEquals("value", PropertySourceChain.ofConstantsClass(ConstantsClass.class).getRequired("config/property").asString()); - - assertEquals("ifacePublic", PropertySourceChain.ofConstantsClass(ConstantsClass.class).orConstantsClass(ConstantsInterface.class).get(ConstantsInterface.IFACE_PUBLIC_TEST).get()); - assertEquals("2", PropertySourceChain.ofConstantsClass(ConstantsClass.class).orConstantsClass(ConstantsInterface.class).get(ConstantsInterface.IFACE_INTEGER).get()); - assertEquals("true", PropertySourceChain.ofConstantsClass(ConstantsClass.class).orConstantsClass(ConstantsInterface.class).get(ConstantsInterface.IFACE_BOOLEAN).get()); - assertEquals("true", PropertySourceChain.ofConstantsClass(ConstantsClass.class).orConstantsClass(ConstantsInterface.class).get(ConstantsClass.booleanProperty).get()); - } - - @Test - public void computation() { - assertEquals("computed", PropertySourceChain.ofComputed(name -> "computed").orFile("test.properties").get("test").get()); - assertEquals("value", PropertySourceChain.ofComputed(name -> "test2".equals(name) ? "computed" : null) - .orFile("test.properties").get("test").get()); - } - - @Test - public void files() throws IOException { - File externalProperties = File.createTempFile("test", ".properties"); - try { - Files.write("test4=value4\n", externalProperties, Charsets.UTF_8); - - assertEquals("value", PropertySourceChain.ofFile("test.properties").get("test").get()); - assertFalse(PropertySourceChain.ofFile("test.properties").get("test2").isPresent()); - - assertEquals("value3", PropertySourceChain.ofFile("test.properties").orFile("test2.properties").get("test3").get()); - assertFalse(PropertySourceChain.ofFile("test.properties").orFile("test2.properties").get("test4").isPresent()); - - assertEquals("value", PropertySourceChain.ofFile("test.properties").getRequired("test").asString()); - assertEquals("value3", PropertySourceChain.ofFile("test.properties").orFile("test2.properties").getRequired("test3").asString()); - - assertEquals("value4", PropertySourceChain.ofFile(externalProperties.getPath()).orFile("test2.properties").getRequired("test4").asString()); - } finally { - externalProperties.delete(); - } - } - - @Test - public void immediates() { - System.setProperty("test", "value"); - - assertEquals("value", PropertySourceChain.ofSystem().orImmediate(d -> d.put("test", "value2")).get("test").get()); - assertEquals("value2", PropertySourceChain.ofSystem().orImmediate(d -> d.put("test2", "value2")).get("test2").get()); - - assertEquals("value", PropertySourceChain.ofSystem().orImmediate(d -> d.put("test", "value2")).getRequired("test").asString()); - assertEquals("value2", PropertySourceChain.ofSystem().orImmediate(d -> d.put("test2", "value2")).getRequired("test2").asString()); - - assertEquals("value2", PropertySourceChain.ofImmediate(d -> d.put("test2", "value2")).getRequired("test2").asString()); - } - - @Test - public void chains() { - environmentVariables.set("test4", "value4"); - System.setProperty("test5", "value5"); - - PropertySource chain = PropertySourceChain - .ofFile("test.properties") - .orFile("nonexistent.proeprties") - .orFile("test2.properties").orSystem().orEnvironment() - .orImmediate(p -> p.put("test6", "value6")); - - assertEquals("value", chain.get("test").get()); - assertEquals("value3", chain.get("test3").get()); - assertEquals("value4", chain.get("test4").get()); - assertEquals("value5", chain.get("test5").get()); - assertEquals("value6", chain.get("test6").get()); - - assertEquals("value", chain.getRequired("test").asString()); - assertEquals("value3", chain.getRequired("test3").asString()); - assertEquals("value4", chain.getRequired("test4").asString()); - assertEquals("value5", chain.getRequired("test5").asString()); - assertEquals("value6", chain.getRequired("test6").asString()); - } - - @Test - public void delegating() { - DelegatingPropertySource delegatingSource = new DelegatingPropertySource(); - assertEquals("value2", PropertySourceChain.ofSource(delegatingSource) - .orImmediate(d -> d.put("test2", "value2")).getRequired("test2").asString()); - - delegatingSource.setDelegate(PropertySourceChain.ofComputed(name -> "value")); - assertEquals("value", PropertySourceChain.ofSource(delegatingSource) - .orImmediate(d -> d.put("test2", "value2")).getRequired("test2").asString()); - } - - @Test - public void transformers() { - environmentVariables.set("PROPERTY_NAME", "value4"); - System.setProperty("PROPERTY2_NAME", "value5"); - - PropertySource base = PropertySourceChain - .ofFile("test.properties") - .orFile("test2.properties") - .orSystem() - .orEnvironment() - .orImmediate(d -> d.put("test6", "value6")); - - PropertySource leaf = PropertySourceChain.ofSource(base).orSource(base, PropertySourceChain.Transformers.LEAF); - PropertySource upper = PropertySourceChain.ofSource(base).orSource(base, - PropertySourceChain.Transformers.compose( - PropertySourceChain.Transformers.LEAF, - PropertySourceChain.Transformers.LOWER_CAMEL_TO_UPPER_UNDERSCORE)); - - assertEquals("value", leaf.get("category/test").get()); - assertEquals("value3", leaf.get("category/test3").get()); - assertEquals("value4", leaf.get("category/PROPERTY_NAME").get()); - assertEquals("value5", leaf.get("category/PROPERTY2_NAME").get()); - assertEquals("value6", leaf.get("category/test6").get()); - - assertEquals("value4", upper.getRequired("category/subcategory/propertyName").asString()); - assertEquals("value5", upper.getRequired("category/subcategory/property2Name").asString()); - } - - @Test - public void missingRequiredProperties() throws Exception { - environmentVariables.set("test4", "value4"); - System.setProperty("test5", "value5"); - - try { - PropertySourceChain.ofEnvironment().getRequired("test3"); - fail(); - } catch (IllegalArgumentException e) { - } - - try { - PropertySourceChain.ofSystem().getRequired("test3"); - fail(); - } catch (IllegalArgumentException e) { - } - - try { - PropertySourceChain.ofFile("test.properties").getRequired("test3"); - fail(); - } catch (IllegalArgumentException e) { - } - - try { - PropertySourceChain.ofFile("test.properties").orFile("test2.properties").getRequired("test4"); - fail(); - } catch (IllegalArgumentException e) { - } - - try { - PropertySourceChain.ofFile("test.properties").orFile("test2.properties").orSystem().orEnvironment() - .orImmediate(d -> d.put("test6", "value6")).getRequired("test2"); - fail(); - } 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"; - } -} diff --git a/core/src/test/java/foundation/stack/datamill/configuration/PropertySourcesTest.java b/core/src/test/java/foundation/stack/datamill/configuration/PropertySourcesTest.java new file mode 100644 index 0000000..4b0a5a0 --- /dev/null +++ b/core/src/test/java/foundation/stack/datamill/configuration/PropertySourcesTest.java @@ -0,0 +1,243 @@ +package foundation.stack.datamill.configuration; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import org.junit.Rule; +import org.junit.Test; +import org.junit.contrib.java.lang.system.EnvironmentVariables; + +import java.io.File; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +/** + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public class PropertySourcesTest { + @Rule + public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); + + @Test + public void environmentVariables() { + environmentVariables.set("test", "value"); + environmentVariables.set("prefix_transformed", "value2"); + + assertEquals("value", PropertySources.fromEnvironment().get("test").get()); + assertFalse(PropertySources.fromEnvironment().get("test2").isPresent()); + + assertFalse(PropertySources.fromEnvironment().get("transformed").isPresent()); + assertEquals("value2", PropertySources.fromEnvironment(v -> "prefix_" + v).get("transformed").get()); + + assertEquals("value", PropertySources.fromEnvironment().getRequired("test").asString()); + } + + @Test + public void systemProperties() { + System.setProperty("test", "value"); + System.setProperty("prefix_transformed", "value2"); + + assertEquals("value", PropertySources.fromSystem().get("test").get()); + assertFalse(PropertySources.fromSystem().get("test2").isPresent()); + + assertFalse(PropertySources.fromSystem().get("transformed").isPresent()); + assertEquals("value2", PropertySources.fromSystem(v -> "prefix_" + v).get("transformed").get()); + + assertEquals("value", PropertySources.fromSystem().getRequired("test").asString()); + } + + @Test + public void constantClasses() { + assertEquals("value", PropertySources.fromConstantsClass(ConstantsClass.class).get(ConstantsClass.property).get()); + assertFalse(PropertySources.fromConstantsClass(ConstantsClass.class).get("property2").isPresent()); + assertFalse(PropertySources.fromConstantsClass(ConstantsClass.class).get("config/instance").isPresent()); + + assertEquals("publicValue", PropertySources.fromConstantsClass(ConstantsClass.class).get(ConstantsClass.publicProperty).get()); + assertEquals("privateValue", PropertySources.fromConstantsClass(ConstantsClass.class).get(ConstantsClass.privateProperty).get()); + assertEquals("nonFinalValue", PropertySources.fromConstantsClass(ConstantsClass.class).get(ConstantsClass.nonFinalProperty).get()); + assertEquals("1", PropertySources.fromConstantsClass(ConstantsClass.class).get(ConstantsClass.integerProperty).get()); + assertEquals("true", PropertySources.fromConstantsClass(ConstantsClass.class).get(ConstantsClass.booleanProperty).get()); + + assertEquals("value", PropertySources.fromConstantsClass(ConstantsClass.class).getRequired("config/property").asString()); + + assertEquals("ifacePublic", PropertySources.fromConstantsClass(ConstantsClass.class).orConstantsClass(ConstantsInterface.class).get(ConstantsInterface.IFACE_PUBLIC_TEST).get()); + assertEquals("2", PropertySources.fromConstantsClass(ConstantsClass.class).orConstantsClass(ConstantsInterface.class).get(ConstantsInterface.IFACE_INTEGER).get()); + assertEquals("true", PropertySources.fromConstantsClass(ConstantsClass.class).orConstantsClass(ConstantsInterface.class).get(ConstantsInterface.IFACE_BOOLEAN).get()); + assertEquals("true", PropertySources.fromConstantsClass(ConstantsClass.class).orConstantsClass(ConstantsInterface.class).get(ConstantsClass.booleanProperty).get()); + } + + @Test + public void computation() { + assertEquals("computed", PropertySources.fromComputed(name -> "computed").orFile("test.properties").get("test").get()); + assertEquals("value", PropertySources.fromComputed(name -> "test2".equals(name) ? "computed" : null) + .orFile("test.properties").get("test").get()); + } + + @Test + public void files() throws IOException { + File externalProperties = File.createTempFile("test", ".properties"); + try { + Files.write("test4=value4\n", externalProperties, Charsets.UTF_8); + + assertEquals("value", PropertySources.fromFile("test.properties").get("test").get()); + assertFalse(PropertySources.fromFile("test.properties").get("test2").isPresent()); + + assertEquals("value3", PropertySources.fromFile("test.properties").orFile("test2.properties").get("test3").get()); + assertFalse(PropertySources.fromFile("test.properties").orFile("test2.properties").get("test4").isPresent()); + + assertEquals("value", PropertySources.fromFile("test.properties").getRequired("test").asString()); + assertEquals("value3", PropertySources.fromFile("test.properties").orFile("test2.properties").getRequired("test3").asString()); + + assertEquals("value4", PropertySources.fromFile(externalProperties.getPath()).orFile("test2.properties").getRequired("test4").asString()); + } finally { + externalProperties.delete(); + } + } + + @Test + public void immediates() { + System.setProperty("test", "value"); + + assertEquals("value", PropertySources.fromSystem().orImmediate(d -> d.put("test", "value2")).get("test").get()); + assertEquals("value2", PropertySources.fromSystem().orImmediate(d -> d.put("test2", "value2")).get("test2").get()); + + assertEquals("value", PropertySources.fromSystem().orImmediate(d -> d.put("test", "value2")).getRequired("test").asString()); + assertEquals("value2", PropertySources.fromSystem().orImmediate(d -> d.put("test2", "value2")).getRequired("test2").asString()); + + assertEquals("value2", PropertySources.fromImmediate(d -> d.put("test2", "value2")).getRequired("test2").asString()); + } + + @Test + public void chains() { + environmentVariables.set("test4", "value4"); + System.setProperty("test5", "value5"); + + PropertySource chain = PropertySources + .fromFile("test.properties") + .orFile("nonexistent.proeprties") + .orFile("test2.properties").orSystem().orEnvironment() + .orImmediate(p -> p.put("test6", "value6")); + + assertEquals("value", chain.get("test").get()); + assertEquals("value3", chain.get("test3").get()); + assertEquals("value4", chain.get("test4").get()); + assertEquals("value5", chain.get("test5").get()); + assertEquals("value6", chain.get("test6").get()); + + assertEquals("value", chain.getRequired("test").asString()); + assertEquals("value3", chain.getRequired("test3").asString()); + assertEquals("value4", chain.getRequired("test4").asString()); + assertEquals("value5", chain.getRequired("test5").asString()); + assertEquals("value6", chain.getRequired("test6").asString()); + } + + @Test + public void conveniences() { + assertEquals("value", PropertySources.fromImmediate(s -> s.put("name", "value")) + .with(s -> s.getRequired("name").asString())); + } + + @Test + public void delegating() { + DelegatingPropertySource delegatingSource = new DelegatingPropertySource(); + assertEquals("value2", PropertySources.from(delegatingSource) + .orImmediate(d -> d.put("test2", "value2")).getRequired("test2").asString()); + + delegatingSource.setDelegate(PropertySources.fromComputed(name -> "value")); + assertEquals("value", PropertySources.from(delegatingSource) + .orImmediate(d -> d.put("test2", "value2")).getRequired("test2").asString()); + } + + @Test + public void transformers() { + environmentVariables.set("PROPERTY_NAME", "value4"); + System.setProperty("PROPERTY2_NAME", "value5"); + + PropertySource base = PropertySources + .fromFile("test.properties") + .orFile("test2.properties") + .orSystem() + .orEnvironment() + .orImmediate(d -> d.put("test6", "value6")); + + PropertySource leaf = PropertySources.from(base).or(base, PropertyNameTransformers.LEAF); + PropertySource upper = PropertySources.from(base).or(base, + PropertyNameTransformers.compose( + PropertyNameTransformers.LEAF, + PropertyNameTransformers.LOWER_CAMEL_TO_UPPER_UNDERSCORE)); + + assertEquals("value", leaf.get("category/test").get()); + assertEquals("value3", leaf.get("category/test3").get()); + assertEquals("value4", leaf.get("category/PROPERTY_NAME").get()); + assertEquals("value5", leaf.get("category/PROPERTY2_NAME").get()); + assertEquals("value6", leaf.get("category/test6").get()); + + assertEquals("value4", upper.getRequired("category/subcategory/propertyName").asString()); + assertEquals("value5", upper.getRequired("category/subcategory/property2Name").asString()); + } + + @Test + public void missingRequiredProperties() throws Exception { + environmentVariables.set("test4", "value4"); + System.setProperty("test5", "value5"); + + try { + PropertySources.fromEnvironment().getRequired("test3"); + fail(); + } catch (IllegalArgumentException e) { + } + + try { + PropertySources.fromSystem().getRequired("test3"); + fail(); + } catch (IllegalArgumentException e) { + } + + try { + PropertySources.fromFile("test.properties").getRequired("test3"); + fail(); + } catch (IllegalArgumentException e) { + } + + try { + PropertySources.fromFile("test.properties").orFile("test2.properties").getRequired("test4"); + fail(); + } catch (IllegalArgumentException e) { + } + + try { + PropertySources.fromFile("test.properties").orFile("test2.properties").orSystem().orEnvironment() + .orImmediate(d -> d.put("test6", "value6")).getRequired("test2"); + fail(); + } catch (IllegalArgumentException e) { + } + } + + private static class ConstantsClass { + @Value("value") + static final String property = "config/property"; + @Value("publicValue") + public static final String publicProperty = "config/public"; + @Value("privateValue") + private static final String privateProperty = "config/private"; + @Value("nonFinalValue") + static String nonFinalProperty = "config/nonFinal"; + @Value("1") + static String integerProperty = "config/integer"; + @Value("true") + static String booleanProperty = "config/boolean"; + @Value("instanceValue") + String instanceProperty = "config/instance"; + } + + private interface ConstantsInterface { + @Value("ifacePublic") + String IFACE_PUBLIC_TEST = "iface/public"; + @Value("2") + String IFACE_INTEGER = "iface/integer"; + @Value("true") + String IFACE_BOOLEAN = "iface/boolean"; + } +} diff --git a/core/src/test/java/foundation/stack/datamill/configuration/WiringTest.java b/core/src/test/java/foundation/stack/datamill/configuration/WiringTest.java index 3d890b2..e6b5815 100644 --- a/core/src/test/java/foundation/stack/datamill/configuration/WiringTest.java +++ b/core/src/test/java/foundation/stack/datamill/configuration/WiringTest.java @@ -1,454 +1,81 @@ package foundation.stack.datamill.configuration; -import foundation.stack.datamill.values.StringValue; import org.junit.Test; -import rx.functions.Func0; -import rx.functions.Func1; -import java.time.LocalDateTime; -import java.util.Optional; - -import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; /** * @author Ravi Chodavarapu (rchodava@gmail.com) */ public class WiringTest { - private static class Test1 { - private final String arg1; - private final String arg2; - - public Test1(@Named("arg1") String arg1, @Named("arg2") String arg2) { - this.arg1 = arg1; - this.arg2 = arg2; - } - } - - private static class Test2 { - private final Func1 func1String; - private final Func0 func0String; - - public Test2(Func0 func0String, Func1 func1String) { - this.func0String = func0String; - this.func1String = func1String; - } - } - - private static class Base { - protected String get() { - return "base"; - } - } - - private static class Derived extends Base { - @Override - protected String get() { - return "derived"; - } - } - - public interface Iface1 { - String iface1(); - } - - public interface Iface2 { - String iface2(); - } - - private static class Interfaces extends Base implements Iface1 { - @Override - public String iface1() { - return "iface1"; - } - } - - private static class DerivedInterfaces extends Interfaces implements Iface2 { - @Override - public String iface2() { - return "iface2"; - } - } - - private static class Test3 { - private final Base base; - private final Func0 stringSupplier; - - public Test3(Base base, Func0 stringSupplier) { - this.base = base; - this.stringSupplier = stringSupplier; + private static class ConcreteClass { + public ConcreteClass() { } } - private static class Test4 { - private final Iface1 iface1; - private final Iface2 iface2; + private static class ConcreteClass2 { + private final ConcreteClass concreteClass; - public Test4(Iface1 iface1, Iface2 iface2) { - this.iface1 = iface1; - this.iface2 = iface2; + public ConcreteClass2(ConcreteClass concreteClass) { + this.concreteClass = concreteClass; } } - private static class Test5 { - private final boolean booleanProperty; - private final Boolean booleanWrapperProperty; - private final byte byteProperty; - private final Byte byteWrapperProperty; - private final char characterProperty; - private final Character characterWrapperProperty; - private final short shortProperty; - private final Short shortWrapperProperty; - private final int integerProperty; - private final Integer integerWrapperProperty; - private final long longProperty; - private final Long longWrapperProperty; - private final float floatProperty; - private final Float floatWrapperProperty; - private final double doubleProperty; - private final Double doubleWrapperProperty; - private final LocalDateTime localDateTimeProperty; - private final byte[] byteArrayProperty; - private final String stringProperty; + private static class ConcreteClass3 { + private final ConcreteClass concreteClass; + private final ConcreteClass2 concreteClass2; - public Test5(@Named("boolean") boolean booleanProperty, @Named("booleanWrapper") Boolean booleanWrapperProperty, - @Named("byte") byte byteProperty, @Named("byteWrapper") Byte byteWrapperProperty, - @Named("char") char characterProperty, @Named("charWrapper") Character characterWrapperProperty, - @Named("short") short shortProperty, @Named("shortWrapper") Short shortWrapperProperty, - @Named("int") int integerProperty, @Named("intWrapper") Integer integerWrapperProperty, - @Named("long") long longProperty, @Named("longWrapper") Long longWrapperProperty, - @Named("float") float floatProperty, @Named("floatWrapper") Float floatWrapperProperty, - @Named("double") double doubleProperty, @Named("doubleWrapper") Double doubleWrapperProperty, - @Named("LocalDateTime") LocalDateTime localDateTimeProperty, - @Named("byteArray") byte[] byteArrayProperty, - @Named("String") String stringProperty) { - this.booleanProperty = booleanProperty; - this.booleanWrapperProperty = booleanWrapperProperty; - this.byteProperty = byteProperty; - this.byteWrapperProperty = byteWrapperProperty; - this.characterProperty = characterProperty; - this.characterWrapperProperty = characterWrapperProperty; - this.shortProperty = shortProperty; - this.shortWrapperProperty = shortWrapperProperty; - this.integerProperty = integerProperty; - this.integerWrapperProperty = integerWrapperProperty; - this.longProperty = longProperty; - this.longWrapperProperty = longWrapperProperty; - this.floatProperty = floatProperty; - this.floatWrapperProperty = floatWrapperProperty; - this.doubleProperty = doubleProperty; - this.doubleWrapperProperty = doubleWrapperProperty; - this.localDateTimeProperty = localDateTimeProperty; - this.byteArrayProperty = byteArrayProperty; - this.stringProperty = stringProperty; - } - } - - private static class Test6 { - private final LocalDateTime localDateTimeProperty; - private final byte[] byteArrayProperty; - - public Test6(LocalDateTime localDateTimeProperty, - byte[] byteArrayProperty) { - this.localDateTimeProperty = localDateTimeProperty; - this.byteArrayProperty = byteArrayProperty; - } - } - - private static class Test7 { - private final Func0 stringSupplier; - - public Test7() { - stringSupplier = () -> "default"; - } - - public Test7(Func0 stringSupplier) { - this.stringSupplier = stringSupplier; - } - } - - private static class Test8 { - private final Test7 test7; - - public Test8(Test7 test7) { - this.test7 = test7; - } - } - - private static class Test9 { - private final Test7 test7; - private final Test8 test8; - - public Test9(Test7 test7, Test8 test8) { - this.test7 = test7; - this.test8 = test8; + public ConcreteClass3(ConcreteClass concreteClass, ConcreteClass2 concreteClass2) { + this.concreteClass = concreteClass; + this.concreteClass2 = concreteClass2; } } @Test public void autoConstruction() { - Test7 test7 = new Test7(); - Test9 test9 = new Wiring().add(test7).construct(Test9.class); - - assertEquals(test7, test9.test7); - assertEquals(test7, test9.test8.test7); - } - - @Test - public void builders() { - Test9 test9 = new Wiring().addFactory(Test7.class, w -> new Test7(() -> "custom")).construct(Test9.class); + ConcreteClass concreteClass = new ConcreteClass(); + ConcreteClass3 concreteClass3 = new Wiring(FactoryChains.forType(ConcreteClass.class, (w, c) -> concreteClass) + .thenForAnyConcreteClass()) + .newInstance(ConcreteClass3.class); - assertEquals("custom", test9.test7.stringSupplier.call()); - assertEquals("custom", test9.test8.test7.stringSupplier.call()); + assertEquals(concreteClass, concreteClass3.concreteClass); + assertEquals(concreteClass, concreteClass3.concreteClass2.concreteClass); } @Test - public void named() { - Test1 instance = new Wiring() - .addNamed("arg1", "value1") - .addNamed("arg2", Optional.of("value2")) - .construct(Test1.class); - - assertEquals("value1", instance.arg1); - assertEquals("value2", instance.arg2); - } - - @Test - public void typed() { - Wiring wiring = new Wiring() - .add((Func0) () -> "func0String", - (Func1) s -> "func1String" + s); - Test2 instance = wiring.construct(Test2.class); - - assertEquals("func0String", instance.func0String.call()); - assertEquals("func1StringS", instance.func1String.call("S")); - - assertEquals("func0String", wiring.get(Func0.class).call()); - assertEquals("func1StringS", wiring.get(Func1.class).call("S")); - } - - @Test - public void parents() { - Test3 instance = new Wiring() - .add(new Derived(), (Func0) () -> "testString") - .construct(Test3.class); - - assertEquals("derived", instance.base.get()); - assertEquals("testString", instance.stringSupplier.call()); - } - - @Test - public void interfaces() { - Wiring wiring = new Wiring() - .add(new DerivedInterfaces()); - - Test4 instance = wiring.construct(Test4.class); - - assertEquals("iface1", instance.iface1.iface1()); - assertEquals("iface2", instance.iface2.iface2()); - - assertEquals("iface1", wiring.get(Iface1.class).iface1()); - assertEquals("iface2", wiring.get(Iface2.class).iface2()); - } - - @Test(expected = IllegalArgumentException.class) - public void noNullAdditions() { - new Wiring().add(null); - } - - @Test(expected = IllegalArgumentException.class) - public void noNullOptionals() { - new Wiring().add(Optional.empty()); - } - - @Test(expected = IllegalArgumentException.class) - public void noNullNamedAdditions() { - new Wiring().addNamed("name", null); - } - - @Test(expected = IllegalArgumentException.class) - public void noNullNamedOptionals() { - new Wiring().addNamed("name", Optional.empty()); - } - - @Test(expected = IllegalArgumentException.class) - public void noNamedDuplicates() { - new Wiring().addNamed("test", "value").addNamed("test", "value2"); - } - - @Test - public void formattedValues() { - Test1 instance = new Wiring().addNamed("arg1", "value") - .addFormatted("arg2", "{0}:{1}", "value1", "value2") - .construct(Test1.class); - - assertEquals("value1:value2", instance.arg2); + public void namedParameters() { + assertEquals("value", new Wiring(PropertySources.fromImmediate(s -> s.put("name", "value"))) + .getNamed("name").map(v -> v.asString()).get()); + assertEquals("value", new Wiring(PropertySources.fromImmediate(s -> s.put("name", "value"))) + .getRequiredNamed("name").asString()); + assertEquals(12, (int) new Wiring(PropertySources.fromImmediate(s -> s.put("name", "12"))) + .getNamed("name").map(v -> v.asInteger()).get()); + + try { + assertEquals("value", new Wiring(PropertySources.fromImmediate(s -> s.put("name", "value"))) + .getRequiredNamed("name2").asString()); + fail("Expected retrieving a non-existent required named property to fail!"); + } catch (IllegalStateException e) { + } } @Test public void conveniences() { - Test1 instance = new Wiring().with(w -> w.addNamed("arg1", "value1").addNamed("arg2", "value2")) - .construct(Test1.class); - - assertEquals("value1", instance.arg1); - assertEquals("value2", instance.arg2); - - instance = new Wiring().performIf(true, w -> w.addNamed("arg1", "true1").addNamed("arg2", "true2")) - .orElse(w -> w.addNamed("arg1", "false1").addNamed("arg2", "false2")) - .construct(Test1.class); - - assertEquals("true1", instance.arg1); - assertEquals("true2", instance.arg2); - - instance = new Wiring().performIf(false, w -> w.addNamed("arg1", "true1").addNamed("arg2", "true2")) - .orElse(w -> w.addNamed("arg1", "false1").addNamed("arg2", "false2")) - .construct(Test1.class); + ConcreteClass instance = new ConcreteClass(); + ConcreteClass instance2 = new ConcreteClass(); - assertEquals("false1", instance.arg1); - assertEquals("false2", instance.arg2); + assertEquals("value", new Wiring(PropertySources.fromImmediate(s -> s.put("name", "value"))) + .with(w -> w.getNamed("name").map(v -> v.asString()).get())); - } - - @Test - public void namedValuesFromPropertySource() { - Wiring wiring = new Wiring() - .setNamedPropertySource(PropertySourceChain.ofSystem().orImmediate(d -> d - .put("boolean", "true") - .put("booleanWrapper", "true") - .put("byte", "1") - .put("byteWrapper", "1") - .put("char", "a") - .put("charWrapper", "a") - .put("short", "2") - .put("shortWrapper", "2") - .put("int", "3") - .put("intWrapper", "3") - .put("long", "4") - .put("longWrapper", "4") - .put("float", "1.1") - .put("floatWrapper", "1.1") - .put("double", "2.2") - .put("doubleWrapper", "2.2") - .put("LocalDateTime", "2007-12-03T10:15:30") - .put("String", "value") - .put("byteArray", "array"))); - Test5 instance = wiring.construct(Test5.class); - - assertEquals(true, instance.booleanProperty); - assertEquals(true, instance.booleanWrapperProperty); - assertEquals(1, instance.byteProperty); - assertEquals(1, (byte) instance.byteWrapperProperty); - assertEquals('a', instance.characterProperty); - assertEquals('a', (char) instance.characterWrapperProperty); - assertEquals(2, instance.shortProperty); - assertEquals(2, (short) instance.shortWrapperProperty); - assertEquals(3, instance.integerProperty); - assertEquals(3, (int) instance.integerWrapperProperty); - assertEquals(4, instance.longProperty); - assertEquals(4, (long) instance.longWrapperProperty); - assertEquals(1.1f, instance.floatProperty, 0.1f); - assertEquals(1.1f, instance.floatWrapperProperty, 0.1f); - assertEquals(2.2d, instance.doubleProperty, 0.1d); - assertEquals(2.2d, instance.doubleWrapperProperty, 0.1d); - assertEquals(LocalDateTime.parse("2007-12-03T10:15:30"), instance.localDateTimeProperty); - assertEquals("value", instance.stringProperty); - assertArrayEquals("array".getBytes(), instance.byteArrayProperty); - - assertEquals(true, wiring.getNamed("boolean").asBoolean()); - assertEquals(true, wiring.getNamed("booleanWrapper").asBoolean()); - assertEquals(1, wiring.getNamed("byte").asByte()); - assertEquals(1, wiring.getNamed("byteWrapper").asByte()); - assertEquals(1.1f, wiring.getNamedAs("floatWrapper", Float.class), 0.001f); - } - - @Test - public void typedNamedValues() { - Test5 instance = new Wiring() - .addNamed("boolean", new StringValue("true")) - .addNamed("booleanWrapper", new StringValue("true")) - .addNamed("byte", new StringValue("1")) - .addNamed("byteWrapper", new StringValue("1")) - .addNamed("char", new StringValue("a")) - .addNamed("charWrapper", new StringValue("a")) - .addNamed("short", new StringValue("2")) - .addNamed("shortWrapper", new StringValue("2")) - .addNamed("int", new StringValue("3")) - .addNamed("intWrapper", new StringValue("3")) - .addNamed("long", new StringValue("4")) - .addNamed("longWrapper", new StringValue("4")) - .addNamed("float", new StringValue("1.1")) - .addNamed("floatWrapper", new StringValue("1.1")) - .addNamed("double", new StringValue("2.2")) - .addNamed("doubleWrapper", new StringValue("2.2")) - .addNamed("LocalDateTime", new StringValue("2007-12-03T10:15:30")) - .addNamed("String", new StringValue("value")) - .addNamed("byteArray", new StringValue("array")) - .construct(Test5.class); - - assertEquals(true, instance.booleanProperty); - assertEquals(true, instance.booleanWrapperProperty); - assertEquals(1, instance.byteProperty); - assertEquals(1, (byte) instance.byteWrapperProperty); - assertEquals('a', instance.characterProperty); - assertEquals('a', (char) instance.characterWrapperProperty); - assertEquals(2, instance.shortProperty); - assertEquals(2, (short) instance.shortWrapperProperty); - assertEquals(3, instance.integerProperty); - assertEquals(3, (int) instance.integerWrapperProperty); - assertEquals(4, instance.longProperty); - assertEquals(4, (long) instance.longWrapperProperty); - assertEquals(1.1f, instance.floatProperty, 0.1f); - assertEquals(1.1f, instance.floatWrapperProperty, 0.1f); - assertEquals(2.2d, instance.doubleProperty, 0.1d); - assertEquals(2.2d, instance.doubleWrapperProperty, 0.1d); - assertEquals(LocalDateTime.parse("2007-12-03T10:15:30"), instance.localDateTimeProperty); - assertEquals("value", instance.stringProperty); - assertArrayEquals("array".getBytes(), instance.byteArrayProperty); - } - - @Test - public void values() { - Wiring wiring = new Wiring() - .add(new StringValue("2007-12-03T10:15:30")) - .add("array".getBytes()); - - Test6 instance = wiring.construct(Test6.class); - - assertEquals(LocalDateTime.parse("2007-12-03T10:15:30"), instance.localDateTimeProperty); - assertArrayEquals("array".getBytes(), instance.byteArrayProperty); - - assertNull(wiring.get(boolean.class)); - assertNull(wiring.get(Boolean.class)); - assertNull(wiring.get(byte.class)); - assertNull(wiring.get(Byte.class)); - assertNull(wiring.get(char.class)); - assertNull(wiring.get(Character.class)); - assertNull(wiring.get(short.class)); - assertNull(wiring.get(Short.class)); - assertNull(wiring.get(int.class)); - assertNull(wiring.get(Integer.class)); - assertNull(wiring.get(long.class)); - assertNull(wiring.get(Long.class)); - assertNull(wiring.get(float.class)); - assertNull(wiring.get(Float.class)); - assertNull(wiring.get(double.class)); - assertNull(wiring.get(Double.class)); - assertNull(wiring.get(String.class)); - assertEquals(LocalDateTime.parse("2007-12-03T10:15:30"), wiring.get(LocalDateTime.class)); - assertArrayEquals("array".getBytes(), wiring.get(byte[].class)); - } - - @Test - public void constructWith() { - Test7 instance = new Wiring() - .add((Func0) () -> "value") - .constructWith(Test7.class); - assertEquals("default", instance.stringSupplier.call()); + assertEquals("value2", new Wiring(PropertySources.fromImmediate(s -> s.put("name", "value"))) + .with(PropertySources.fromImmediate(s -> s.put("name", "value2")), + w -> w.getNamed("name").map(v -> v.asString()).get())); - instance = new Wiring() - .add((Func0) () -> "value") - .constructWith(Test7.class, Func0.class); - assertEquals("value", instance.stringSupplier.call()); + // Test that sub-wirings created using 'with' share the same scopes + Wiring original = new Wiring(FactoryChains.forType(ConcreteClass.class, w -> instance)); + original.singleton(ConcreteClass.class); + assertEquals(instance, original.with(FactoryChains.forType(ConcreteClass.class, w -> instance2), + w -> w.singleton(ConcreteClass.class))); } } diff --git a/core/src/test/java/foundation/stack/datamill/configuration/impl/ConcreteClassFactoryTest.java b/core/src/test/java/foundation/stack/datamill/configuration/impl/ConcreteClassFactoryTest.java new file mode 100644 index 0000000..728f3b1 --- /dev/null +++ b/core/src/test/java/foundation/stack/datamill/configuration/impl/ConcreteClassFactoryTest.java @@ -0,0 +1,188 @@ +package foundation.stack.datamill.configuration.impl; + +import foundation.stack.datamill.configuration.Named; +import foundation.stack.datamill.configuration.PropertySources; +import foundation.stack.datamill.configuration.Wiring; +import foundation.stack.datamill.configuration.WiringException; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public class ConcreteClassFactoryTest { + private static final Logger logger = LoggerFactory.getLogger(ConcreteClassFactoryTest.class); + + @Test + public void construction() { + assertTrue(new Wiring().singleton(ConcreteClass.class) instanceof ConcreteClass); + + assertTrue(new Wiring().singleton(ConcreteClass2.class) instanceof ConcreteClass2); + assertTrue(new Wiring().singleton(ConcreteClass2.class).concreteClass instanceof ConcreteClass); + + assertTrue(new Wiring().singleton(ConcreteClass3.class) instanceof ConcreteClass3); + assertTrue(new Wiring().singleton(ConcreteClass3.class).concreteClass instanceof ConcreteClass); + assertTrue(new Wiring().singleton(ConcreteClass3.class).concreteClass2 instanceof ConcreteClass2); + + assertTrue(new Wiring(PropertySources.fromImmediate(s -> s.put("named", "12"))) + .singleton(ConcreteClassWithNamedParameters.class).concreteClass instanceof ConcreteClass); + assertEquals(12, new Wiring(PropertySources.fromImmediate(s -> s.put("named", "12"))) + .singleton(ConcreteClassWithNamedParameters.class).named); + } + + @Test + public void failures() { + try { + new Wiring().singleton(FailureNoPublicConstructors.class); + fail("Should have failed to construct class with no public constructors!"); + } catch (WiringException e) { + logger.debug("\n" + e.toString()); + } + + try { + new Wiring().singleton(FailureDependencyOnClassWithNoPublicConstructors.class); + fail("Should have failed to construct class which depends on another class with no public constructors!"); + } catch (WiringException e) { + logger.debug("\n" + e.toString()); + } + + try { + new Wiring().singleton(FailureDependenciesOnClassesWithNoPublicConstructors.class); + fail("Should have failed to construct class which depends on other classes with no public constructors!"); + } catch (WiringException e) { + logger.debug("\n" + e.toString()); + } + + try { + new Wiring().singleton(FailureUnsatisfiedDependencies.class); + fail("Should have failed to construct class which has unsatisfied dependencies!"); + } catch (WiringException e) { + logger.debug("\n" + e.toString()); + } + + try { + new Wiring().singleton(FailureDependencyOnClassWithUnsatisfiedDependencies.class); + fail("Should have failed to construct class which depends on another class with unsatisfied dependencies!"); + } catch (WiringException e) { + logger.debug("\n" + e.toString()); + } + + try { + new Wiring().singleton(FailureUnsatisfiedNamedParameters.class); + fail("Should have failed to construct class which has unsatisfied named parameters!"); + } catch (WiringException e) { + logger.debug("\n" + e.toString()); + } + + try { + new Wiring().singleton(FailureDependencyOnClassWithUnsatisfiedNamedParameters.class); + fail("Should have failed to construct class which depends on another class with unsatisfied named parameters!"); + } catch (WiringException e) { + logger.debug("\n" + e.toString()); + } + + try { + new Wiring(PropertySources.fromImmediate(s -> s.put("named", "abcd"))) + .singleton(FailureInvalidNamedParameters.class); + fail("Should have failed to construct class which has an invalid named parameter value!"); + } catch (WiringException e) { + logger.debug("\n" + e.toString()); + } + } + + private static class ConcreteClass { + public ConcreteClass() { + } + } + + private static class ConcreteClass2 { + private ConcreteClass concreteClass; + + public ConcreteClass2(ConcreteClass concreteClass) { + this.concreteClass = concreteClass; + } + } + + private static class ConcreteClass3 { + private ConcreteClass concreteClass; + private ConcreteClass2 concreteClass2; + + public ConcreteClass3(ConcreteClass concreteClass, ConcreteClass2 concreteClass2) { + this.concreteClass = concreteClass; + this.concreteClass2 = concreteClass2; + } + } + + private static class ConcreteClassWithNamedParameters { + private ConcreteClass concreteClass; + private int named; + + public ConcreteClassWithNamedParameters(ConcreteClass concreteClass, @Named("named") int named) { + this.concreteClass = concreteClass; + this.named = named; + } + } + + private static class FailureNoPublicConstructors { + private FailureNoPublicConstructors() { + } + + protected FailureNoPublicConstructors(int __) { + } + } + + private static class FailureDependencyOnClassWithNoPublicConstructors { + public FailureDependencyOnClassWithNoPublicConstructors(FailureNoPublicConstructors __) { + } + } + + private static class FailureDependenciesOnClassesWithNoPublicConstructors { + public FailureDependenciesOnClassesWithNoPublicConstructors(FailureNoPublicConstructors __) { + } + + public FailureDependenciesOnClassesWithNoPublicConstructors(FailureDependencyOnClassWithNoPublicConstructors __) { + } + } + + private static class FailureUnsatisfiedDependencies { + public FailureUnsatisfiedDependencies(List __) { + } + + public FailureUnsatisfiedDependencies(String __, boolean ___) { + + } + } + + private static class FailureDependencyOnClassWithUnsatisfiedDependencies { + public FailureDependencyOnClassWithUnsatisfiedDependencies(FailureUnsatisfiedDependencies __) { + } + } + + private static class FailureUnsatisfiedNamedParameters { + public FailureUnsatisfiedNamedParameters(@Named("named1") String __) { + + } + + public FailureUnsatisfiedNamedParameters(@Named("named2") boolean __) { + + } + } + + private static class FailureDependencyOnClassWithUnsatisfiedNamedParameters { + public FailureDependencyOnClassWithUnsatisfiedNamedParameters(FailureUnsatisfiedNamedParameters __) { + } + } + + private static class FailureInvalidNamedParameters { + public FailureInvalidNamedParameters(@Named("named") int __) { + + } + } +} diff --git a/core/src/test/java/foundation/stack/datamill/configuration/impl/SimpleValueConverterTest.java b/core/src/test/java/foundation/stack/datamill/configuration/impl/SimpleValueConverterTest.java new file mode 100644 index 0000000..1b283b5 --- /dev/null +++ b/core/src/test/java/foundation/stack/datamill/configuration/impl/SimpleValueConverterTest.java @@ -0,0 +1,53 @@ +package foundation.stack.datamill.configuration.impl; + +import foundation.stack.datamill.values.StringValue; +import foundation.stack.datamill.values.Value; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public class SimpleValueConverterTest { + @Test + public void convert() throws Exception { + assertEquals(true, SimpleValueConverter.convert(true, boolean.class)); + assertEquals(12, (byte) SimpleValueConverter.convert(12, byte.class)); + assertEquals('c', (char) SimpleValueConverter.convert('c', char.class)); + assertEquals(12, (int) SimpleValueConverter.convert(12, int.class)); + assertEquals(12, (short) SimpleValueConverter.convert(12, short.class)); + assertEquals(12l, (long) SimpleValueConverter.convert(12, long.class));; + assertEquals(12f, SimpleValueConverter.convert(12f, float.class), 0e-32f); + assertEquals(12d, SimpleValueConverter.convert(12d, double.class), 0e-32f); + + assertEquals(true, SimpleValueConverter.convert(true, Boolean.class)); + assertEquals(12, (byte) SimpleValueConverter.convert(12, Byte.class)); + assertEquals('c', (char) SimpleValueConverter.convert('c', Character.class)); + assertEquals(12, (int) SimpleValueConverter.convert(12, Integer.class)); + assertEquals(12, (short) SimpleValueConverter.convert(12, Short.class)); + assertEquals(12l, (long) SimpleValueConverter.convert(12, Long.class));; + assertEquals(12f, SimpleValueConverter.convert(12f, Float.class), 0e-32f); + assertEquals(12d, SimpleValueConverter.convert(12d, Double.class), 0e-32f); + + assertEquals(true, SimpleValueConverter.convert(new StringValue("true"), boolean.class)); + assertEquals(12, (byte) SimpleValueConverter.convert(new StringValue("12"), byte.class)); + assertEquals('c', (char) SimpleValueConverter.convert(new StringValue("c"), char.class)); + assertEquals(12, (int) SimpleValueConverter.convert(new StringValue("12"), int.class)); + assertEquals(12, (short) SimpleValueConverter.convert(new StringValue("12"), short.class)); + assertEquals(12l, (long) SimpleValueConverter.convert(new StringValue("12"), long.class));; + assertEquals(12f, SimpleValueConverter.convert(new StringValue("12"), float.class), 0e-32f); + assertEquals(12d, SimpleValueConverter.convert(new StringValue("12"), double.class), 0e-32f); + assertEquals("Test", SimpleValueConverter.convert(new StringValue("Test"), String.class)); + assertEquals("Test", SimpleValueConverter.convert("Test", Value.class).asString()); + + assertEquals(true, SimpleValueConverter.convert(new StringValue("true"), Boolean.class)); + assertEquals(12, (byte) SimpleValueConverter.convert(new StringValue("12"), Byte.class)); + assertEquals('c', (char) SimpleValueConverter.convert(new StringValue("c"), Character.class)); + assertEquals(12, (int) SimpleValueConverter.convert(new StringValue("12"), Integer.class)); + assertEquals(12, (short) SimpleValueConverter.convert(new StringValue("12"), Short.class)); + assertEquals(12l, (long) SimpleValueConverter.convert(new StringValue("12"), Long.class));; + assertEquals(12f, SimpleValueConverter.convert(new StringValue("12"), Float.class), 0e-32f); + assertEquals(12d, SimpleValueConverter.convert(new StringValue("12"), Double.class), 0e-32f); + } +} diff --git a/cucumber/src/main/java/foundation/stack/datamill/cucumber/PlaceholderResolver.java b/cucumber/src/main/java/foundation/stack/datamill/cucumber/PlaceholderResolver.java index 8011378..011e6e7 100644 --- a/cucumber/src/main/java/foundation/stack/datamill/cucumber/PlaceholderResolver.java +++ b/cucumber/src/main/java/foundation/stack/datamill/cucumber/PlaceholderResolver.java @@ -1,7 +1,7 @@ package foundation.stack.datamill.cucumber; import com.jayway.jsonpath.JsonPath; -import foundation.stack.datamill.configuration.PropertySourceChain; +import foundation.stack.datamill.configuration.PropertySources; import foundation.stack.datamill.values.Value; import foundation.stack.datamill.security.impl.BCrypt; import org.slf4j.Logger; @@ -129,7 +129,7 @@ private String resolveDateFormatterPlaceholder(String key) { private String resolveSystemPlaceholder(String key) { if (key.startsWith(SYSTEM_PLACEHOLDER_PREFIX)) { key = key.substring(SYSTEM_PLACEHOLDER_PREFIX.length()); - return PropertySourceChain.ofSystem().get(key).orElse(null); + return PropertySources.fromSystem().get(key).orElse(null); } return null; From b4af376587064dde4935cc739219f977fc514d4e Mon Sep 17 00:00:00 2001 From: Ravi Chodavarapu Date: Mon, 17 Apr 2017 00:49:31 +0100 Subject: [PATCH 5/5] Break recursion when a factory calls Wiring again to construct an instance Super of isn't necessarily going to return an instance of a class derived from the class it is registered with - for example (registering super of Class1, but a call to the factory can request construction of an interface of Class1, for which the factory returns Class2, which implements the interface but is not a sub-class of Class1) Add additional unit tests Fixed bug so that the the singleton scope respects qualifiers Added lots of documentation --- .../datamill/configuration/FactoryChain.java | 14 ++- .../datamill/configuration/FactoryChains.java | 51 ++++++++- .../ImmediatePropertySource.java | 5 + .../configuration/PropertySources.java | 14 ++- .../configuration/QualifyingFactory.java | 2 +- .../configuration/SingletonScope.java | 40 ++++++- .../stack/datamill/configuration/Wiring.java | 105 ++++++++++++------ .../impl/ConcreteClassFactory.java | 42 ++++++- .../configuration/impl/FactoryChainImpl.java | 54 +++++++-- .../datamill/http/builder/ElseBuilder.java | 2 +- .../datamill/http/builder/RouteBuilder.java | 2 +- .../datamill/http/impl/RouteBuilderImpl.java | 4 +- .../configuration/FactoryChainsTest.java | 12 ++ .../configuration/PropertySourcesTest.java | 4 + .../datamill/configuration/WiringTest.java | 22 ++++ .../impl/ConcreteClassFactoryTest.java | 13 +++ 16 files changed, 313 insertions(+), 73 deletions(-) diff --git a/core/src/main/java/foundation/stack/datamill/configuration/FactoryChain.java b/core/src/main/java/foundation/stack/datamill/configuration/FactoryChain.java index 5663f60..3c68c0b 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/FactoryChain.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/FactoryChain.java @@ -1,9 +1,17 @@ package foundation.stack.datamill.configuration; /** + * A chain of object factories used by {@link Wiring}s to create objects. Use {@link FactoryChains} to create + * {@link FactoryChain}s. + * * @author Ravi Chodavarapu (rchodava@gmail.com) */ public interface FactoryChain extends QualifyingFactory { + /** + * Remove a factory from the chain, returning a chain without the specified factory. + */ + FactoryChain exclude(QualifyingFactory factory); + /** * Add a factory which will be used to attempt construction of instances for all types. */ @@ -18,7 +26,7 @@ public interface FactoryChain extends QualifyingFactory { * Add a factory for a type, and it's super-classes and interfaces. Note that this factory will be invoked if the * type requested is exactly the specified type, or one of it's super-classes or interfaces. */ - FactoryChain thenForSuperOf(Class type, Factory factory); + FactoryChain thenForSuperOf(Class type, Factory factory); /** * Add a factory for a specific type. Note that this factory is only invoked if the type being constructed matches @@ -36,7 +44,7 @@ public interface FactoryChain extends QualifyingFactory { /** * @see #thenForSuperOf(Class, Factory) */ - FactoryChain thenForSuperOf(Class type, TypeLessFactory factory); + FactoryChain thenForSuperOf(Class type, TypeLessFactory factory); /** * @see #thenForType(Class, Factory) @@ -55,7 +63,7 @@ public interface FactoryChain extends QualifyingFactory { * * @see #thenForSuperOf(Class, Factory) */ - FactoryChain thenForSuperOf(Class type, QualifyingFactory factory); + FactoryChain thenForSuperOf(Class type, QualifyingFactory factory); /** * Add a qualifying factory for a specific type. diff --git a/core/src/main/java/foundation/stack/datamill/configuration/FactoryChains.java b/core/src/main/java/foundation/stack/datamill/configuration/FactoryChains.java index 715766f..e280b9b 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/FactoryChains.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/FactoryChains.java @@ -4,6 +4,51 @@ import foundation.stack.datamill.configuration.impl.FactoryChainImpl; /** + *

+ * Use this class to create{@link FactoryChain}s. For example, consider a chain composed and used as follows: + *

+ *

+ * chain = FactoryChains.forType(DatabaseClient.class, w -> createDatabaseClient())
+ *     .thenForSuperOf(UserRepository.class, w -> new UserRepository(...))
+ *     .thenForAny(Core.FACTORY)
+ *     .thenForAnyConcreteClass();
+ * 
+ *

+ * This chain will first check if the class requested is a DatabaseClient, and if so, will construct it. Then, it will + * check if the class requested is a super-class or interface of the UserRepository class, and if so will construct it. + * Then, it will delegate to the Core.FACTORY factory chain. Finally, if none of those factories are able to construct + * the requested instance, the chain will ask the concrete class factory to construct an instance. + *

+ *

+ * Note that if you do not add the concrete class factory at the end of a chain, your chain will only construct the + * exact types for which your factories can construct instances. Thus, it is often useful to add the concrete class + * factory to the end of a factory chain. + *

+ *

+ * Note that it is easy to construct factory chains that result in infinite recursion. For example, consider the + * following: + *

+ *
+ * chain = FactoryChains.forSuperOf(EmailService.class, w -> w.singleton(EmailService.class))
+ *     .thenForAnyConcreteClass();
+ *
+ * new Wiring(chain).singleton(EmailService.class);
+ * 
+ *

+ * If the chain is used in this way to construct an instance of the EmailService class (a concrete class), the first + * factory in the chain is used. This results in turn to a call to the wiring to construct a singleton instance of + * EmailService. The Wiring will use the same chain, starting at the beginning of the chain. This will again result + * in the first factory in the chain to be used, and would lead to an infinite recursion. To solve this, when a particular + * factory in the chain in turn uses the Wiring to construct an instance, the factory will be excluded from the chain in + * the next call. This will essentially break the infinite recursion - the call to construct the EmailService from the + * first factory in the chain will go directly to the concrete class factory in the second pass through the chain. + *

+ *

+ * Note that factory chains are immutable, and each of the chaining methods returns a new chain with the additional + * factory added. This allows you to have common chains which you can combine in different ways, because each + * of those combinations is a new chain. + *

+ * * @author Ravi Chodavarapu (rchodava@gmail.com) */ public class FactoryChains { @@ -24,7 +69,7 @@ public static FactoryChain forAny(Factory factory) { /** * @see FactoryChain#thenForSuperOf(Class, Factory) */ - public static FactoryChain forSuperOf(Class type, Factory factory) { + public static FactoryChain forSuperOf(Class type, Factory factory) { return new FactoryChainImpl(FactoryChainImpl.isSuperOf(type), factory); } @@ -45,7 +90,7 @@ public static FactoryChain forAny(QualifyingFactory facto /** * @see FactoryChain#thenForSuperOf(Class, QualifyingFactory) */ - public static FactoryChain forSuperOf(Class type, QualifyingFactory factory) { + public static FactoryChain forSuperOf(Class type, QualifyingFactory factory) { return new FactoryChainImpl(FactoryChainImpl.isSuperOf(type), factory); } @@ -66,7 +111,7 @@ public static FactoryChain forAny(TypeLessFactory factory) { /** * @see FactoryChain#thenForSuperOf(Class, TypeLessFactory) */ - public static FactoryChain forSuperOf(Class type, TypeLessFactory factory) { + public static FactoryChain forSuperOf(Class type, TypeLessFactory factory) { return new FactoryChainImpl(FactoryChainImpl.isSuperOf(type), factory); } diff --git a/core/src/main/java/foundation/stack/datamill/configuration/ImmediatePropertySource.java b/core/src/main/java/foundation/stack/datamill/configuration/ImmediatePropertySource.java index c1ab60f..ecb86c7 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/ImmediatePropertySource.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/ImmediatePropertySource.java @@ -8,5 +8,10 @@ */ public interface ImmediatePropertySource { ImmediatePropertySource put(String name, String value); + + /** + * Add a formatted value to the immediate set of properties. Uses the + * {@link java.text.MessageFormat#format(String, Object...)} method to format the value. + */ ImmediatePropertySource put(String name, String format, Object... arguments); } diff --git a/core/src/main/java/foundation/stack/datamill/configuration/PropertySources.java b/core/src/main/java/foundation/stack/datamill/configuration/PropertySources.java index fb2e383..728bc42 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/PropertySources.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/PropertySources.java @@ -31,13 +31,13 @@ * transformers. *

*

- * Note that each of the chaining methods returns a new chain with the additional property source added. This allows you - * to have common chains which you can combine in different ways, because each of those combinations is a new chain. For - * example: + * Note that property chains are immutable, and each of the chaining methods returns a new chain with the additional + * property source added. This allows you to have common chains which you can combine in different ways, because each + * of those combinations is a new chain. For example: *

*
- * chain1 = COMMON.orEnvironment(PropertyNameTransformers.LOWER_CAMEL_TO_UPPER_UNDERSCORE);
- * chain2 = COMMON.orFile("defaults.properties");
+ * chain1 = common.orEnvironment(PropertyNameTransformers.LOWER_CAMEL_TO_UPPER_UNDERSCORE);
+ * chain2 = common.orFile("defaults.properties");
  * 
*

* Each of these chains is a separate chain that has some common section. @@ -127,7 +127,9 @@ public static PropertySourceChain fromImmediate(Action1 /** * @see PropertySourceChain#orImmediate(Action1, Func1) */ - public static PropertySourceChain fromImmediate(Action1 initializer, Func1 transformer) { + public static PropertySourceChain fromImmediate( + Action1 initializer, + Func1 transformer) { ImmediatePropertySourceImpl immediateSource = new ImmediatePropertySourceImpl(); initializer.call(immediateSource); diff --git a/core/src/main/java/foundation/stack/datamill/configuration/QualifyingFactory.java b/core/src/main/java/foundation/stack/datamill/configuration/QualifyingFactory.java index 34f6ca3..bb75621 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/QualifyingFactory.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/QualifyingFactory.java @@ -6,7 +6,7 @@ /** * An object factory like {@link Factory} but one that requires qualifiers (see {@link Qualifier}) to be present before - * it constructs objects. The qualifying factory is passed a set of qualifiers (as a {@link Collection} of strings to + * it constructs objects. The qualifying factory is passed a set of qualifiers (as a {@link Collection} of Strings to * allow the factory to test which qualifiers are present. * * @author Ravi Chodavarapu (rchodava@gmail.com) diff --git a/core/src/main/java/foundation/stack/datamill/configuration/SingletonScope.java b/core/src/main/java/foundation/stack/datamill/configuration/SingletonScope.java index bc01ae3..5da1566 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/SingletonScope.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/SingletonScope.java @@ -1,14 +1,12 @@ package foundation.stack.datamill.configuration; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; +import java.util.*; /** * @author Ravi Chodavarapu (rchodava@gmail.com) */ public class SingletonScope implements Scope { - private Map, Object> constructed = new HashMap<>(); + private Map constructed = new HashMap<>(); @Override public R resolve( @@ -16,14 +14,44 @@ public R resolve( QualifyingFactory factory, Class type, Collection qualifiers) { - Object instance = constructed.get(type); + Object instance = constructed.get(new QualifiedType(type, qualifiers)); if (instance == null) { instance = factory.call(wiring, type, qualifiers); if (instance != null) { - constructed.put(type, instance); + constructed.put(new QualifiedType(type, qualifiers), instance); } } return (R) instance; } + + private static class QualifiedType { + private final Class type; + private final String[] qualifiers; + + public QualifiedType(Class type, Collection qualifiers) { + this.type = type; + this.qualifiers = qualifiers != null && qualifiers.size() > 0 ? + qualifiers.toArray(new String[qualifiers.size()]) : null; + } + + @Override + public int hashCode() { + return qualifiers != null ? + Objects.hash(type, Arrays.hashCode(qualifiers)) : + type.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof QualifiedType) { + QualifiedType other = (QualifiedType) obj; + return qualifiers != null ? + Arrays.equals(qualifiers, other.qualifiers) && type == other.type : + type == other.type; + } + + return false; + } + } } 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 5ca4cad..c332f6f 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/Wiring.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/Wiring.java @@ -26,16 +26,15 @@ *

* You can use a Wiring to create a UserRepository instance: *

- * UserRepository repository = new Wiring()
- *     .newInstance(UserRepository.class);
+ * UserRepository repository = new Wiring().newInstance(UserRepository.class);
  * 
*

*

* This constructs a new UserRepository using the public constructor, creating an instance of DataStore, and any * dependencies DataStore may have in the process. How did the wiring know how to construct DataStore and it's * dependencies? By default, a Wiring will automatically create instances of concrete classes for which it can - * transitively create dependencies. But what if we have an interface or a class that doesn't have dependencies that - * can be satisfied? You setup the Wiring with a {@link FactoryChain} that tells it how to construct specific classes. + * transitively create dependencies. But what if we have an interface, or a class that has dependencies that we cannot + * be satisfied? You setup the Wiring with a {@link FactoryChain} that tells it how to construct specific classes. * For example, consider: *

*
@@ -45,32 +44,66 @@
  * }
  * 
*
- * FactoryChain chain = FactoryChains.forType(DatabaseClient.class, w -> new DatabaseClient(...))
+ * FactoryChain chain =
+ *     FactoryChains.forType(DatabaseClient.class, w -> new DatabaseClient(...))
  *         .thenForType(OutlineBuilder.class, w -> OutlineBuilder.DEFAULT)
  *         .thenForAnyConcreteClass();
  * ApplicationRepository repository = new Wiring(chain).newInstance(ApplicationRepository.class);
  * 
*

* Note that the {@link FactoryChain} we built and passed in to the Wiring will tell it exactly how to build objects of - * the types it encounters. The chain will be consulted in the order it is composed - in this case, first, if the - * Wiring needs to construct a DatabaseClient, it will use the first lambda provided. If it needs an OutlineBuilder, it - * will use the second lambda and for all other concrete classes, it will attempt to auto-construct them. It will use - * the chain every time a dependency needs to be constructed. Use {@link FactoryChains} as a starting point for creating - * factory chains. + * the types it encounters. The chain will be consulted in the order it is composed - in this case, when the Wiring + * needs to construct a DatabaseClient, it will use the first lambda provided. If it needs an OutlineBuilder, it + * will use the second lambda and for all other concrete classes, it will use a concrete class factory to construct them. + * It will use the chain every time a dependency needs to be constructed, starting at the beginning. Use + * {@link FactoryChains} as a starting point for creating factory chains. *

*

* Notice that in the previous examples we called the newInstance method on the Wiring. This will create a new instance * of that class every single time. While this is sometimes the desired behavior, most of the time, you will want to * use {@link Wiring#singleton(Class)} instead to create a singleton instance of a type. If you call singleton to * construct instances of some type multiple times, you will get back the same singleton instance every time. The - * singletons are scoped to the {@link Wiring}'s default singleton {@link Scope}. A different {@link Scope} can be - * passed in to a Wiring when it is constructed to change how instances are scoped. If you pass in a different scope, - * you can use the {@link Wiring#defaultScoped(Class)} method to construct instances using that scope. + * singletons are scoped to the {@link Wiring} itself, and it's default singleton {@link Scope}. A different + * {@link Scope} can be passed in to a Wiring when it is constructed to change how instances are scoped. If you pass in + * a different scope, you can use the {@link Wiring#defaultScoped(Class)} method to construct instances using that scope. *

*

+ * Occasionally, you may have multiple parameters of the same type in a constructor and you want to have the Wiring + * create different instances for each parameter. For example, consider: + *

+ *
+ * public class UserController {
+ *     public UserController(Func1<ServerRequest, Boolean> authenticationFilter, Func1<ServerRequest, Boolean> authorizationFilter) {
+ *     }
+ * }
+ * 
+ *

+ * You can add {@link Qualifier}s to these parameters to tell the Wiring to inject different instances: + *

+ *
+ * public class UserController {
+ *     public UserController(
+ *         @Qualifier("authentication") Func1<ServerRequest, Boolean> authenticationFilter,
+ *         @Qualifier("authorization") Func1<ServerRequest, Boolean> authorizationFilter) {
+ *     }
+ * }
+ * 
+ *

+ * You can now add a {@link QualifyingFactory} to your {@link FactoryChain} in order to construct different instances + * based on the qualifier: + *

+ *
+ * FactoryChain chain =
+ *     FactoryChains.forType(Func1.class, (w, c, q) -> q.contains("authentication") ?
+ *             authenticationFilter() : authorizationFilter())
+ *         .thenForAnyConcreteClass();
+ * UserController controller = new Wiring(chain).singleton(UserController.class);
+ * 
+ *

* In addition to using the factory chain to create dependencies, a Wiring will also resolve any {@link Named} * constructor parameters using a {@link PropertySource} it is setup with. This is useful for picking up externalized - * properties from constants classes, files, environment variables and system properties. For example, consider: + * properties from files, environment variables, system properties, constants classes and other sources. For example, + * consider: *

  * public class DatabaseClient {
  *     public DatabaseClient(@Named("url") String url, @Named("username") String username, @Named("password") String password) {
@@ -102,27 +135,27 @@
  */
 public class Wiring {
     private final PropertySource propertySource;
-    private final QualifyingFactory factory;
+    private final FactoryChain factoryChain;
     private final Scope defaultScope;
     private final Scope singletonScope;
 
-    public Wiring(PropertySource propertySource, QualifyingFactory factory, Scope defaultScope) {
+    public Wiring(PropertySource propertySource, FactoryChain factoryChain, Scope defaultScope) {
         this.propertySource = propertySource;
         this.singletonScope = new SingletonScope();
         this.defaultScope = defaultScope != null ? defaultScope : singletonScope;
-        this.factory = factory;
+        this.factoryChain = factoryChain;
     }
 
-    public Wiring(PropertySource propertySource, QualifyingFactory factory) {
-        this(propertySource, factory, null);
+    public Wiring(PropertySource propertySource, FactoryChain factoryChain) {
+        this(propertySource, factoryChain, null);
     }
 
-    public Wiring(QualifyingFactory factory, Scope defaultScope) {
-        this(null, factory, defaultScope);
+    public Wiring(FactoryChain factoryChain, Scope defaultScope) {
+        this(null, factoryChain, defaultScope);
     }
 
-    public Wiring(QualifyingFactory factory) {
-        this(factory, null);
+    public Wiring(FactoryChain factoryChain) {
+        this(factoryChain, null);
     }
 
     public Wiring(PropertySource propertySource, Scope defaultScope) {
@@ -137,11 +170,19 @@ public Wiring() {
         this((PropertySource) null);
     }
 
-    private Wiring(PropertySource propertySource, QualifyingFactory factory, Scope defaultScope, Scope singletonScope) {
+    public Wiring(Wiring wiring, FactoryChain factoryChain) {
+        this(wiring.propertySource, factoryChain, wiring.defaultScope, wiring.singletonScope);
+    }
+
+    private Wiring(PropertySource propertySource, FactoryChain factoryChain, Scope defaultScope, Scope singletonScope) {
         this.propertySource = propertySource;
         this.singletonScope = singletonScope;
         this.defaultScope = defaultScope;
-        this.factory = factory;
+        this.factoryChain = factoryChain;
+    }
+
+    public FactoryChain getFactoryChain() {
+        return factoryChain;
     }
 
     public Value getRequiredNamed(String property) {
@@ -157,8 +198,8 @@ public Optional getNamed(String property) {
 
     private  R construct(Scope scope, Class type, Collection qualifiers) {
         return scope != null ?
-                (R) scope.resolve(this, factory, (Class) type, qualifiers) :
-                (R) factory.call(this, (Class) type, qualifiers);
+                (R) scope.resolve(this, factoryChain, (Class) type, qualifiers) :
+                (R) factoryChain.call(this, (Class) type, qualifiers);
     }
 
     public  R defaultScoped(Class type) {
@@ -197,16 +238,16 @@ public  R singleton(Class type, Collection qualifiers
         return construct(singletonScope, type, qualifiers);
     }
 
-    public  R with(PropertySource propertySource, QualifyingFactory factory, Func1 wiringConsumer) {
-        return wiringConsumer.call(new Wiring(propertySource, factory, defaultScope, singletonScope));
+    public  R with(PropertySource propertySource, FactoryChain factoryChain, Func1 wiringConsumer) {
+        return wiringConsumer.call(new Wiring(propertySource, factoryChain, defaultScope, singletonScope));
     }
 
     public  R with(PropertySource propertySource, Func1 wiringConsumer) {
-        return with(propertySource, factory, wiringConsumer);
+        return with(propertySource, factoryChain, wiringConsumer);
     }
 
-    public  R with(QualifyingFactory factory, Func1 wiringConsumer) {
-        return with(propertySource, factory, wiringConsumer);
+    public  R with(FactoryChain factoryChain, Func1 wiringConsumer) {
+        return with(propertySource, factoryChain, wiringConsumer);
     }
 
     public  R with(Func1 wiringConsumer) {
diff --git a/core/src/main/java/foundation/stack/datamill/configuration/impl/ConcreteClassFactory.java b/core/src/main/java/foundation/stack/datamill/configuration/impl/ConcreteClassFactory.java
index 60626fb..6a7bc9c 100644
--- a/core/src/main/java/foundation/stack/datamill/configuration/impl/ConcreteClassFactory.java
+++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/ConcreteClassFactory.java
@@ -1,5 +1,6 @@
 package foundation.stack.datamill.configuration.impl;
 
+import foundation.stack.datamill.configuration.Qualifier;
 import foundation.stack.datamill.configuration.QualifyingFactory;
 import foundation.stack.datamill.configuration.Wiring;
 import foundation.stack.datamill.configuration.WiringException;
@@ -21,6 +22,20 @@
 public class ConcreteClassFactory implements QualifyingFactory {
     private static final ConcreteClassFactory INSTANCE = new ConcreteClassFactory<>();
 
+    private static void appendException(StringBuilder message, Throwable e) {
+        message.append(e.getClass().getName());
+
+        if (e.getCause() != null && e.getCause() != e) {
+            message.append(": ");
+            appendException(message, e.getCause());
+        }
+
+        if (e.getMessage() != null) {
+            message.append(": ");
+            message.append(e.getMessage());
+        }
+    }
+
     private static  Constructor[] getPublicConstructors(Class type) {
         Constructor[] constructors = type.getConstructors();
         if (constructors == null || constructors.length == 0) {
@@ -64,17 +79,14 @@ private static void throwFailedInstantiationException(Constructor constructor
         StringBuilder message = new StringBuilder("Failed to instantiate using constructor [");
         message.append(constructor.getName());
         message.append("] - ");
-        message.append(e.getMessage());
+        appendException(message, e);
         throw new WiringException(message.toString());
     }
-
     private static void throwInvalidParameterValue(Parameter parameter, IllegalArgumentException e) {
         StringBuilder message = new StringBuilder("Value retrieved for parameter [");
         message.append(parameter.getName());
         message.append("] was invalid: ");
-        message.append(e.getClass().getName());
-        message.append(", ");
-        message.append(e.getMessage());
+        appendException(message, e);
 
         throw new WiringException(message.toString());
     }
@@ -116,6 +128,24 @@ private R construct(Wiring wiring, Class type) {
         return null;
     }
 
+    private Collection retrieveQualifierIfPresent(Parameter parameter) {
+        Qualifier[] qualifiers = parameter.getAnnotationsByType(Qualifier.class);
+        if (qualifiers != null) {
+            ArrayList qualifierNames = new ArrayList<>();
+            for (Qualifier qualifier : qualifiers) {
+                if (qualifier.value() != null) {
+                    qualifierNames.add(qualifier.value());
+                }
+            }
+
+            if (!qualifierNames.isEmpty()) {
+                return qualifierNames;
+            }
+        }
+
+        return Collections.emptyList();
+    }
+
     private R constructWithConstructor(
             Wiring wiring,
             Constructor constructor) {
@@ -125,7 +155,7 @@ private R constructWithConstructor(
         for (int i = 0; i < parameters.length; i++) {
             Class parameterType = parameters[i].getType();
             try {
-                values[i] = wiring.defaultScoped(parameterType);
+                values[i] = wiring.defaultScoped(parameterType, retrieveQualifierIfPresent(parameters[i]));
                 if (values[i] == null) {
                     try {
                         values[i] = NamedParameterValueRetriever.retrieveValueIfNamedParameter(wiring, parameters[i]);
diff --git a/core/src/main/java/foundation/stack/datamill/configuration/impl/FactoryChainImpl.java b/core/src/main/java/foundation/stack/datamill/configuration/impl/FactoryChainImpl.java
index 552a469..c1790d1 100644
--- a/core/src/main/java/foundation/stack/datamill/configuration/impl/FactoryChainImpl.java
+++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/FactoryChainImpl.java
@@ -12,19 +12,22 @@
  * @author Ravi Chodavarapu (rchodava@gmail.com)
  */
 public class FactoryChainImpl implements FactoryChain {
-    public static  Predicate> isExactType(Class factoryType) {
+    private static final Predicate> TAUTOLOGY = __ -> true;
+
+    public static Predicate> isExactType(Class factoryType) {
         return type -> type == factoryType;
     }
 
-    public static  Predicate> isSuperOf(Class factoryType) {
+    public static Predicate> isSuperOf(Class factoryType) {
         return type -> type.isAssignableFrom(factoryType);
     }
 
     public static Predicate> tautology() {
-        return __ -> true;
+        return TAUTOLOGY;
     }
 
     private final List delegates;
+    private final List> exclusions;
 
     public FactoryChainImpl(Predicate> condition, TypeLessFactory factory) {
         this(condition, Factory.wrap(factory));
@@ -36,17 +39,27 @@ public FactoryChainImpl(Predicate> condition, Factory factory) {
 
     public FactoryChainImpl(Predicate> condition, QualifyingFactory factory) {
         this.delegates = Collections.singletonList(new FactoryDelegate(condition, factory));
+        this.exclusions = null;
     }
 
     private FactoryChainImpl(List delegates) {
+        this(delegates, null);
+    }
+
+    private FactoryChainImpl(List delegates, List> exclusions) {
         this.delegates = delegates;
+        this.exclusions = exclusions;
     }
 
     @Override
     public Object call(Wiring wiring, Class type, Collection qualifiers) {
         for (FactoryDelegate delegate : delegates) {
-            if (delegate.condition.test(type)) {
-                Object constructed = delegate.factory.call(wiring, (Class) type, qualifiers);
+            if (delegate.condition.test(type) &&
+                    (exclusions == null || !exclusions.contains(delegate.factory))) {
+                Wiring childWiring = shouldExcludeDelegateFactory(delegate) ?
+                        new Wiring(wiring, wiring.getFactoryChain().exclude(delegate.factory)) : wiring;
+
+                Object constructed = delegate.factory.call(childWiring, (Class) type, qualifiers);
                 if (constructed != null) {
                     return constructed;
                 }
@@ -56,10 +69,21 @@ public Object call(Wiring wiring, Class type, Collection delegates = new ArrayList<>(this.delegates);
-        delegates.add(delegate);
-        return new FactoryChainImpl(delegates);
+    @Override
+    public FactoryChain exclude(QualifyingFactory factory) {
+        ArrayList> exclusions =
+                new ArrayList<>(this.exclusions != null ? this.exclusions.size() + 1 : 1);
+        if (this.exclusions != null) {
+            exclusions.addAll(this.exclusions);
+        }
+
+        exclusions.add(factory);
+
+        return new FactoryChainImpl(delegates, exclusions);
+    }
+
+    private boolean shouldExcludeDelegateFactory(FactoryDelegate delegate) {
+        return delegate.factory != ConcreteClassFactory.instance() && delegate.condition != TAUTOLOGY;
     }
 
     @Override
@@ -73,7 +97,7 @@ public FactoryChain thenForAnyConcreteClass() {
     }
 
     @Override
-    public  FactoryChain thenForSuperOf(Class type, Factory factory) {
+    public  FactoryChain thenForSuperOf(Class type, Factory factory) {
         return thenForSuperOf((Class) type, QualifyingFactory.wrap(factory));
     }
 
@@ -88,7 +112,7 @@ public  FactoryChain thenForAny(TypeLessFactory factory) {
     }
 
     @Override
-    public  FactoryChain thenForSuperOf(Class type, TypeLessFactory factory) {
+    public  FactoryChain thenForSuperOf(Class type, TypeLessFactory factory) {
         return thenForSuperOf((Class) type, Factory.wrap(factory));
     }
 
@@ -103,7 +127,7 @@ public  FactoryChain thenForAny(QualifyingFactory factory)
     }
 
     @Override
-    public  FactoryChain thenForSuperOf(Class type, QualifyingFactory factory) {
+    public  FactoryChain thenForSuperOf(Class type, QualifyingFactory factory) {
         return withAppendedDelegate(new FactoryDelegate(isSuperOf(type), factory));
     }
 
@@ -112,6 +136,12 @@ public  FactoryChain thenForType(Class type, QualifyingFactor
         return withAppendedDelegate(new FactoryDelegate(isExactType(type), factory));
     }
 
+    private FactoryChain withAppendedDelegate(FactoryDelegate delegate) {
+        ArrayList delegates = new ArrayList<>(this.delegates);
+        delegates.add(delegate);
+        return new FactoryChainImpl(delegates);
+    }
+
     private static class FactoryDelegate {
         private final Predicate> condition;
         private final QualifyingFactory factory;
diff --git a/core/src/main/java/foundation/stack/datamill/http/builder/ElseBuilder.java b/core/src/main/java/foundation/stack/datamill/http/builder/ElseBuilder.java
index f966629..4e38b96 100644
--- a/core/src/main/java/foundation/stack/datamill/http/builder/ElseBuilder.java
+++ b/core/src/main/java/foundation/stack/datamill/http/builder/ElseBuilder.java
@@ -18,7 +18,7 @@ public interface ElseBuilder {
     ElseBuilder elseIfMethodMatches(foundation.stack.datamill.http.Method method, Route route);
     ElseBuilder elseIfUriMatches(String pattern, Route route);
     ElseBuilder elseIfMethodAndUriMatch(foundation.stack.datamill.http.Method method, String pattern, Route route);
-     ElseBuilder elseIfMatchesBeanMethod(T bean);
+     ElseBuilder elseIfMatchesInstanceMethod(T bean);
     ElseBuilder elseIfMatchesBeanMethod(Bean bean);
     ElseBuilder elseIfMatchesBeanMethod(
             Bean bean,
diff --git a/core/src/main/java/foundation/stack/datamill/http/builder/RouteBuilder.java b/core/src/main/java/foundation/stack/datamill/http/builder/RouteBuilder.java
index e526af7..858918f 100644
--- a/core/src/main/java/foundation/stack/datamill/http/builder/RouteBuilder.java
+++ b/core/src/main/java/foundation/stack/datamill/http/builder/RouteBuilder.java
@@ -17,7 +17,7 @@ public interface RouteBuilder {
     ElseBuilder ifUriMatches(String pattern, Route route);
     ElseBuilder ifMethodMatches(Method method, Route route);
     ElseBuilder ifMethodAndUriMatch(Method method, String pattern, Route route);
-     ElseBuilder ifMatchesBeanMethod(T bean);
+     ElseBuilder ifMatchesInstanceMethod(T bean);
     ElseBuilder ifMatchesBeanMethod(Bean bean);
     ElseBuilder ifMatchesBeanMethod(
             Bean bean,
diff --git a/core/src/main/java/foundation/stack/datamill/http/impl/RouteBuilderImpl.java b/core/src/main/java/foundation/stack/datamill/http/impl/RouteBuilderImpl.java
index 3dc98fb..a1abc2c 100644
--- a/core/src/main/java/foundation/stack/datamill/http/impl/RouteBuilderImpl.java
+++ b/core/src/main/java/foundation/stack/datamill/http/impl/RouteBuilderImpl.java
@@ -22,7 +22,7 @@ public class RouteBuilderImpl implements RouteBuilder, ElseBuilder {
     private final List matchers = new ArrayList<>();
 
     @Override
-    public  ElseBuilder elseIfMatchesBeanMethod(T bean) {
+    public  ElseBuilder elseIfMatchesInstanceMethod(T bean) {
         return elseIfMatchesBeanMethod(OutlineBuilder.DEFAULT.wrap(bean));
     }
 
@@ -52,7 +52,7 @@ public ElseBuilder elseIfUriMatches(String pattern, Route route) {
     }
 
     @Override
-    public  ElseBuilder ifMatchesBeanMethod(T bean) {
+    public  ElseBuilder ifMatchesInstanceMethod(T bean) {
         return ifMatchesBeanMethod(OutlineBuilder.DEFAULT.wrap(bean));
     }
 
diff --git a/core/src/test/java/foundation/stack/datamill/configuration/FactoryChainsTest.java b/core/src/test/java/foundation/stack/datamill/configuration/FactoryChainsTest.java
index 3993ac6..b99818d 100644
--- a/core/src/test/java/foundation/stack/datamill/configuration/FactoryChainsTest.java
+++ b/core/src/test/java/foundation/stack/datamill/configuration/FactoryChainsTest.java
@@ -3,6 +3,7 @@
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 /**
@@ -44,6 +45,17 @@ public void chains() {
                 .singleton(DerivedClass.class) != derived);
     }
 
+    @Test
+    public void noInfiniteRecursion() {
+        assertNotNull(
+                new Wiring(FactoryChains.forType(ConcreteClass.class, w -> w.singleton(ConcreteClass.class))
+                        .thenForAnyConcreteClass()).singleton(ConcreteClass.class));
+
+        assertNotNull(
+                new Wiring(FactoryChains.forSuperOf(DerivedClass.class, w -> w.singleton(ConcreteClass.class))
+                        .thenForAnyConcreteClass()).singleton(ConcreteClass.class));
+    }
+
     @Test
     public void typedFactories() {
         ConcreteClass instance = new ConcreteClass();
diff --git a/core/src/test/java/foundation/stack/datamill/configuration/PropertySourcesTest.java b/core/src/test/java/foundation/stack/datamill/configuration/PropertySourcesTest.java
index 4b0a5a0..45c5db5 100644
--- a/core/src/test/java/foundation/stack/datamill/configuration/PropertySourcesTest.java
+++ b/core/src/test/java/foundation/stack/datamill/configuration/PropertySourcesTest.java
@@ -77,6 +77,8 @@ public void computation() {
 
     @Test
     public void files() throws IOException {
+        assertFalse(PropertySources.fromFile("nonexistent.properties").get("test2").isPresent());
+
         File externalProperties = File.createTempFile("test", ".properties");
         try {
             Files.write("test4=value4\n", externalProperties, Charsets.UTF_8);
@@ -107,6 +109,8 @@ public void immediates() {
         assertEquals("value2", PropertySources.fromSystem().orImmediate(d -> d.put("test2", "value2")).getRequired("test2").asString());
 
         assertEquals("value2", PropertySources.fromImmediate(d -> d.put("test2", "value2")).getRequired("test2").asString());
+
+        assertEquals("value-value2", PropertySources.fromImmediate(d -> d.put("test", "{0}-{1}","value", "value2")).getRequired("test").asString());
     }
 
     @Test
diff --git a/core/src/test/java/foundation/stack/datamill/configuration/WiringTest.java b/core/src/test/java/foundation/stack/datamill/configuration/WiringTest.java
index e6b5815..ba31dfa 100644
--- a/core/src/test/java/foundation/stack/datamill/configuration/WiringTest.java
+++ b/core/src/test/java/foundation/stack/datamill/configuration/WiringTest.java
@@ -3,6 +3,7 @@
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 /**
@@ -78,4 +79,25 @@ public void conveniences() {
         assertEquals(instance, original.with(FactoryChains.forType(ConcreteClass.class, w -> instance2),
                 w -> w.singleton(ConcreteClass.class)));
     }
+
+    @Test
+    public void scopes() {
+        Wiring wiring = new Wiring(FactoryChains.forAnyConcreteClass());
+        ConcreteClass concreteClass = wiring.newInstance(ConcreteClass.class);
+        assertTrue(concreteClass != wiring.newInstance(ConcreteClass.class));
+
+        wiring = new Wiring(FactoryChains.forAnyConcreteClass());
+        concreteClass = wiring.singleton(ConcreteClass.class);
+        assertEquals(concreteClass, wiring.singleton(ConcreteClass.class));
+
+        wiring = new Wiring(FactoryChains.forAnyConcreteClass());
+        concreteClass = wiring.singleton(ConcreteClass.class, "qualifier");
+        assertEquals(concreteClass, wiring.singleton(ConcreteClass.class, "qualifier"));
+        assertTrue(concreteClass != wiring.singleton(ConcreteClass.class, "qualifier2"));
+
+        wiring = new Wiring(FactoryChains.forAnyConcreteClass());
+        concreteClass = wiring.singleton(ConcreteClass.class, "qualifier1", "qualifier2");
+        assertEquals(concreteClass, wiring.singleton(ConcreteClass.class, "qualifier1", "qualifier2"));
+        assertTrue(concreteClass != wiring.singleton(ConcreteClass.class, "qualifier2", "qualifier3"));
+    }
 }
diff --git a/core/src/test/java/foundation/stack/datamill/configuration/impl/ConcreteClassFactoryTest.java b/core/src/test/java/foundation/stack/datamill/configuration/impl/ConcreteClassFactoryTest.java
index 728f3b1..aaf5459 100644
--- a/core/src/test/java/foundation/stack/datamill/configuration/impl/ConcreteClassFactoryTest.java
+++ b/core/src/test/java/foundation/stack/datamill/configuration/impl/ConcreteClassFactoryTest.java
@@ -95,6 +95,13 @@ public void failures() {
         } catch (WiringException e) {
             logger.debug("\n" + e.toString());
         }
+
+        try {
+            new Wiring().singleton(FailureThrowingConstructor.class);
+            fail("Should have failed to construct class with a throwing constructor!");
+        } catch (WiringException e) {
+            logger.debug("\n" + e.toString());
+        }
     }
 
     private static class ConcreteClass {
@@ -185,4 +192,10 @@ public FailureInvalidNamedParameters(@Named("named") int __) {
 
         }
     }
+
+    private static class FailureThrowingConstructor {
+        public FailureThrowingConstructor() {
+            throw new IllegalArgumentException("Error!");
+        }
+    }
 }