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/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..3c68c0b --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/FactoryChain.java @@ -0,0 +1,74 @@ +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. + */ + 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..e280b9b --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/FactoryChains.java @@ -0,0 +1,124 @@ +package foundation.stack.datamill.configuration; + +import foundation.stack.datamill.configuration.impl.ConcreteClassFactory; +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 { + /** + * @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 new file mode 100644 index 0000000..ecb86c7 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/ImmediatePropertySource.java @@ -0,0 +1,17 @@ +package foundation.stack.datamill.configuration; + +/** + * Used in defining an immediate immutable set of properties. + * + * @see PropertySources + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +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/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 213a780..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,14 +3,14 @@ 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) */ @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 deleted file mode 100644 index e7f07e6..0000000 --- a/core/src/main/java/foundation/stack/datamill/configuration/Properties.java +++ /dev/null @@ -1,48 +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#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/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 ea7fe0e..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,25 +1,16 @@ 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. + * A source of properties. Use {@link PropertySources} 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. * @@ -43,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 3ffc770..bd6df5c 100644 --- a/core/src/main/java/foundation/stack/datamill/configuration/PropertySourceChain.java +++ b/core/src/main/java/foundation/stack/datamill/configuration/PropertySourceChain.java @@ -4,38 +4,50 @@ import rx.functions.Func1; /** - * Allows {@link PropertySource}s to be linked 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.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. - *

- * Note that each of the chaining methods returns a new chain with the additional property source added. - * * @author Ravi Chodavarapu (rchodava@gmail.com) */ public interface PropertySourceChain extends PropertySource { /** - * @see PropertySource#alias(String, String) + * Add a custom property source to the chain at this point. + * + * @param source Source to add to the chain. */ - PropertySourceChain alias(String alias, String original); + PropertySourceChain or(PropertySource source); /** - * Add a property source to the chain which retrieves properties specified in the file at the specified path. + * @See PropertySources + * @see #or(PropertySource) + */ + PropertySourceChain or(PropertySource source, Func1 transformer); + + /** + * 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 path Path to properties file to add as a source. + * @param computation Computation function for the source. */ - PropertySourceChain orFile(String path); + PropertySourceChain orComputed(Func1 computation); + + /** + * @See PropertySources + * @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 Value} annotations. + * + * @param constantsClass Constants class to add as a source. + */ + PropertySourceChain orConstantsClass(Class constantsClass); + + /** + * @See PropertySources + * @see #orConstantsClass(Class) + */ + PropertySourceChain orConstantsClass(Class constantsClass, Func1 transformer); /** * Add a source which looks up properties defined as environment variables to the chain. @@ -43,39 +55,45 @@ public interface PropertySourceChain extends PropertySource { PropertySourceChain orEnvironment(); /** - * 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 PropertySources + * @see #orEnvironment() */ PropertySourceChain orEnvironment(Func1 transformer); /** - * Add a custom property source to the chain at this point.. + * Add a property source to the chain which retrieves properties specified in the file at the specified path. * - * @param source Source to add to the chain. + * @param path Path to properties file to add as a source. */ - PropertySourceChain orSource(PropertySource source); + PropertySourceChain orFile(String path); /** - * Add a source which looks up properties defined as system properties to the chain. + * @See PropertySources + * @see #orFile(String) */ - PropertySourceChain orSystem(); + PropertySourceChain orFile(String path, Func1 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. + * Add immediate set of properties that can be looked up at this point in the chain. * - * @see #orEnvironment(Func1) + * @param initializer Function which sets up immediate properties at this point in the chain. */ - PropertySourceChain orSystem(Func1 transformer); + PropertySourceChain orImmediate(Action1 initializer); /** - * 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. + * @See PropertySources + * @see #orImmediate(Action1) */ - PropertySource orDefaults(Action1 defaultsInitializer); + PropertySourceChain orImmediate(Action1 initializer, Func1 transformer); + + /** + * Add a source which looks up properties defined as system properties to the chain. + */ + PropertySourceChain orSystem(); + + /** + * @See PropertySources + * @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..728bc42 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/PropertySources.java @@ -0,0 +1,153 @@ +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 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");
+ * 
+ *

+ * 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..bb75621 --- /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..5da1566 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/SingletonScope.java @@ -0,0 +1,57 @@ +package foundation.stack.datamill.configuration; + +import java.util.*; + +/** + * @author Ravi Chodavarapu (rchodava@gmail.com) + */ +public class SingletonScope implements Scope { + private Map constructed = new HashMap<>(); + + @Override + public R resolve( + Wiring wiring, + QualifyingFactory factory, + Class type, + Collection qualifiers) { + Object instance = constructed.get(new QualifiedType(type, qualifiers)); + if (instance == null) { + instance = factory.call(wiring, type, qualifiers); + if (instance != null) { + 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/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/Value.java b/core/src/main/java/foundation/stack/datamill/configuration/Value.java new file mode 100644 index 0000000..b550c52 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/Value.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: + * + * \@Value("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 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 947fc73..c332f6f 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,712 +18,239 @@ * 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);
+ * UserRepository repository = new Wiring().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 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: *

+ *
+ * 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, 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. *

*

- * 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} 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. *

*

- * 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: + * 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 DatabaseClient {
- *     public DatabaseClient(@Named("url") String url, @Named("username") String username, @Named("password") String password) {
+ * public class UserController {
+ *     public UserController(Func1<ServerRequest, Boolean> authenticationFilter, Func1<ServerRequest, Boolean> authorizationFilter) {
  *     }
  * }
  * 
- *

*

- * You can use a Wiring to construct a DatabaseClient using the named parameters: + * You can add {@link Qualifier}s to these parameters to tell the Wiring to inject different instances: + *

*
- * DatabaseClient client = new Wiring()
- *     .addFormatted("url", "jdbc:mysql://{0}:{1}/{2}", "localhost", 3306, "database")
- *     .addNamed("username", "dbuser")
- *     .addNamed("password", "dbpass")
- *     .construct(DatabaseClient.class);
+ * 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);
+ * 
*

- * 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. + * 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 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) {
+ *     }
+ * }
+ * 
*

*

- * 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 use a Wiring to construct a DatabaseClient using the named parameters: + *

+ * 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);
+ * 
*

*

- * 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"); - } + private final PropertySource propertySource; + private final FactoryChain factoryChain; + private final Scope defaultScope; + private final Scope singletonScope; - 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"); - } - - 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, FactoryChain factoryChain, Scope defaultScope) { + this.propertySource = propertySource; + this.singletonScope = new SingletonScope(); + this.defaultScope = defaultScope != null ? defaultScope : singletonScope; + this.factoryChain = factoryChain; } - /** - * 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, FactoryChain factoryChain) { + this(propertySource, factoryChain, 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(FactoryChain factoryChain, Scope defaultScope) { + this(null, factoryChain, 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(FactoryChain factoryChain) { + this(factoryChain, null); } - /** - * 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); + public Wiring(PropertySource propertySource, Scope defaultScope) { + this(propertySource, FactoryChains.forAnyConcreteClass(), defaultScope); } - /** - * 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) { + this(propertySource, (Scope) null); } - /** - * 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); + public Wiring() { + this((PropertySource) null); } - 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 { - logger.error("Could not build class {} as the following type was not found {}", clazz, parameterType); - return null; - } - } - } - - return instantiate(clazz, constructor, values); + public Wiring(Wiring wiring, FactoryChain factoryChain) { + this(wiring.propertySource, factoryChain, wiring.defaultScope, wiring.singletonScope); } - 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); - } + private Wiring(PropertySource propertySource, FactoryChain factoryChain, Scope defaultScope, Scope singletonScope) { + this.propertySource = propertySource; + this.singletonScope = singletonScope; + this.defaultScope = defaultScope; + this.factoryChain = factoryChain; } - 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; + public FactoryChain getFactoryChain() { + return factoryChain; } - 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, factoryChain, (Class) type, qualifiers) : + (R) factoryChain.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, FactoryChain factoryChain, Func1 wiringConsumer) { + return wiringConsumer.call(new Wiring(propertySource, factoryChain, 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, factoryChain, wiringConsumer); } - public interface ElseBuilder { - Wiring orElseDoNothing(); - - Wiring orElse(Action1 consumer); + public R with(FactoryChain factoryChain, Func1 wiringConsumer) { + return with(propertySource, factoryChain, 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 49b2ced..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,37 +3,19 @@ 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.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 @@ -47,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/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/ConcreteClassFactory.java b/core/src/main/java/foundation/stack/datamill/configuration/impl/ConcreteClassFactory.java new file mode 100644 index 0000000..6a7bc9c --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/ConcreteClassFactory.java @@ -0,0 +1,196 @@ +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; + +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 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) { + 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("] - "); + 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: "); + appendException(message, e); + + 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 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) { + 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, retrieveQualifierIfPresent(parameters[i])); + 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 new file mode 100644 index 0000000..c012cc1 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/ConstantsClassSource.java @@ -0,0 +1,73 @@ +package foundation.stack.datamill.configuration.impl; + +import foundation.stack.datamill.configuration.Value; +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) { + Value value = field.getAnnotation(Value.class); + if (value != null) { + constants.put(propertyName, value.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/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/FactoryChainImpl.java b/core/src/main/java/foundation/stack/datamill/configuration/impl/FactoryChainImpl.java new file mode 100644 index 0000000..c1790d1 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/FactoryChainImpl.java @@ -0,0 +1,154 @@ +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 { + private static final Predicate> TAUTOLOGY = __ -> true; + + 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 TAUTOLOGY; + } + + private final List delegates; + private final List> exclusions; + + 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)); + 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) && + (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; + } + } + } + + return null; + } + + @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 + 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 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; + + 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 new file mode 100644 index 0000000..6fac8f9 --- /dev/null +++ b/core/src/main/java/foundation/stack/datamill/configuration/impl/ImmediatePropertySourceImpl.java @@ -0,0 +1,42 @@ +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; + +/** + * @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; + } + + @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 index 1d8242d..3eb7723 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 @@ -1,8 +1,6 @@ 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 foundation.stack.datamill.configuration.*; import rx.functions.Action1; import rx.functions.Func1; @@ -16,25 +14,22 @@ * @author Ravi Chodavarapu (rchodava@gmail.com) */ public class PropertySourceChainImpl extends AbstractSource implements PropertySourceChain { - private final List chain; + private static final Func1 IDENTITY = name -> name; - public PropertySourceChainImpl(PropertySource initialSource) { - chain = Collections.singletonList(initialSource); - } + private final List chain; - private PropertySourceChainImpl(List chain) { - this.chain = chain; + public PropertySourceChainImpl(PropertySource initialSource, Func1 transformer) { + chain = Collections.singletonList(new TransformedSource(initialSource, transformer)); } - @Override - public PropertySourceChain alias(String alias, String original) { - return (PropertySourceChain) super.alias(alias, original); + private PropertySourceChainImpl(List chain) { + this.chain = chain; } @Override public Optional getOptional(String name) { - for (PropertySource source : chain) { - Optional value = source.get(name); + for (TransformedSource source : chain) { + Optional value = source.propertySource.get(source.transformer.call(name)); if (value.isPresent()) { return value; } @@ -44,47 +39,92 @@ public Optional getOptional(String name) { } @Override - public PropertySource orDefaults(Action1 defaultsInitializer) { - DefaultsSource defaults = new DefaultsSource(); - defaultsInitializer.call(defaults); + 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); + } - return orSource(defaults); + @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 orSource(EnvironmentPropertiesSource.IDENTITY); + return orEnvironment(null); } @Override public PropertySourceChain orEnvironment(Func1 transformer) { - return orSource(new EnvironmentPropertiesSource(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 orSource(new FileSource(path)); + return or(new FileSource(path), transformer); } catch (IOException e) { - return orSource(EmptySource.INSTANCE); + return or(EmptySource.INSTANCE); } } @Override - public PropertySourceChain orSource(PropertySource source) { - ArrayList newChain = new ArrayList<>(chain); - newChain.add(source); + public PropertySourceChain orImmediate(Action1 initializer) { + return orImmediate(initializer, null); + } - return new PropertySourceChainImpl(newChain); + @Override + public PropertySourceChain orImmediate(Action1 initializer, Func1 transformer) { + ImmediatePropertySourceImpl defaults = new ImmediatePropertySourceImpl(); + initializer.call(defaults); + + return or(defaults, transformer); } @Override public PropertySourceChain orSystem() { - return orSource(SystemPropertiesSource.IDENTITY); + return orSystem(null); } @Override public PropertySourceChain orSystem(Func1 transformer) { - return orSource(new SystemPropertiesSource(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/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/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) { + + } + } } 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 new file mode 100644 index 0000000..b99818d --- /dev/null +++ b/core/src/test/java/foundation/stack/datamill/configuration/FactoryChainsTest.java @@ -0,0 +1,127 @@ +package foundation.stack.datamill.configuration; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +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 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(); + 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/PropertiesTest.java b/core/src/test/java/foundation/stack/datamill/configuration/PropertiesTest.java deleted file mode 100644 index d054ca5..0000000 --- a/core/src/test/java/foundation/stack/datamill/configuration/PropertiesTest.java +++ /dev/null @@ -1,134 +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 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) { - } - } -} 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..45c5db5 --- /dev/null +++ b/core/src/test/java/foundation/stack/datamill/configuration/PropertySourcesTest.java @@ -0,0 +1,247 @@ +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 { + assertFalse(PropertySources.fromFile("nonexistent.properties").get("test2").isPresent()); + + 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()); + + assertEquals("value-value2", PropertySources.fromImmediate(d -> d.put("test", "{0}-{1}","value", "value2")).getRequired("test").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 824480f..ba31dfa 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,103 @@ 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.assertTrue; +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 ConcreteClass { + public ConcreteClass() { } } - private static class Test2 { - private final Func1 func1String; - private final Func0 func0String; + private static class ConcreteClass2 { + private final ConcreteClass concreteClass; - public Test2(Func0 func0String, Func1 func1String) { - this.func0String = func0String; - this.func1String = func1String; + public ConcreteClass2(ConcreteClass concreteClass) { + this.concreteClass = concreteClass; } } - private static class Base { - protected String get() { - return "base"; - } - } + private static class ConcreteClass3 { + private final ConcreteClass concreteClass; + private final ConcreteClass2 concreteClass2; - 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 Test4 { - private final Iface1 iface1; - private final Iface2 iface2; - - public Test4(Iface1 iface1, Iface2 iface2) { - this.iface1 = iface1; - this.iface2 = iface2; - } - } - - 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; - - 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); - - assertEquals("custom", test9.test7.stringSupplier.call()); - assertEquals("custom", test9.test8.test7.stringSupplier.call()); - } - - @Test - public void named() { - Test1 instance = new Wiring() - .addNamed("arg1", "value1") - .addNamed("arg2", Optional.of("value2")) - .construct(Test1.class); + ConcreteClass concreteClass = new ConcreteClass(); + ConcreteClass3 concreteClass3 = new Wiring(FactoryChains.forType(ConcreteClass.class, (w, c) -> concreteClass) + .thenForAnyConcreteClass()) + .newInstance(ConcreteClass3.class); - assertEquals("value1", instance.arg1); - assertEquals("value2", instance.arg2); + assertEquals(concreteClass, concreteClass3.concreteClass); + assertEquals(concreteClass, concreteClass3.concreteClass2.concreteClass); } @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); - - assertEquals("false1", instance.arg1); - assertEquals("false2", instance.arg2); - - } + ConcreteClass instance = new ConcreteClass(); + ConcreteClass instance2 = new ConcreteClass(); - @Test - public void namedValuesFromPropertySource() { - Wiring wiring = new Wiring() - .setNamedPropertySource(Properties.fromSystem().orDefaults(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("value", new Wiring(PropertySources.fromImmediate(s -> s.put("name", "value"))) + .with(w -> w.getNamed("name").map(v -> v.asString()).get())); - 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()); + 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())); - 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 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))); } @Test - public void constructWith() { - Test7 instance = new Wiring() - .add((Func0) () -> "value") - .constructWith(Test7.class); - assertEquals("default", instance.stringSupplier.call()); - - instance = new Wiring() - .add((Func0) () -> "value") - .constructWith(Test7.class, Func0.class); - assertEquals("value", instance.stringSupplier.call()); + 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 new file mode 100644 index 0000000..aaf5459 --- /dev/null +++ b/core/src/test/java/foundation/stack/datamill/configuration/impl/ConcreteClassFactoryTest.java @@ -0,0 +1,201 @@ +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()); + } + + 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 { + 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 __) { + + } + } + + private static class FailureThrowingConstructor { + public FailureThrowingConstructor() { + throw new IllegalArgumentException("Error!"); + } + } +} 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 962651f..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.Properties; +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 Properties.fromSystem().get(key).orElse(null); + return PropertySources.fromSystem().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\"" + "}")