diff --git a/governator-core/src/main/java/com/netflix/governator/PropertiesPropertySource.java b/governator-core/src/main/java/com/netflix/governator/PropertiesPropertySource.java index e0bc3d6c..7d8901c4 100644 --- a/governator-core/src/main/java/com/netflix/governator/PropertiesPropertySource.java +++ b/governator-core/src/main/java/com/netflix/governator/PropertiesPropertySource.java @@ -4,21 +4,35 @@ import javax.inject.Singleton; +import com.google.inject.Binder; import com.google.inject.Module; import com.google.inject.Provides; import com.netflix.governator.spi.PropertySource; -public class PropertiesPropertySource extends AbstractPropertySource { +public final class PropertiesPropertySource extends AbstractPropertySource implements Module { private Properties props; public PropertiesPropertySource(Properties props) { this.props = props; } + public PropertiesPropertySource() { + this(new Properties()); + } + public static PropertiesPropertySource from(Properties props) { return new PropertiesPropertySource(props); } + public PropertiesPropertySource setProperty(String key, String value) { + props.setProperty(key, value); + return this; + } + + public boolean hasProperty(String key, String value) { + return props.containsKey(key); + } + public static Module toModule(final Properties props) { return new SingletonModule() { @Provides @@ -38,4 +52,28 @@ public String get(String key) { public String get(String key, String defaultValue) { return props.getProperty(key, defaultValue); } + + @Override + public void configure(Binder binder) { + binder.bind(PropertySource.class).toInstance(this); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + + throw new RuntimeException("Only one PropertiesModule may be installed"); + } + + } diff --git a/governator-core/src/main/java/com/netflix/governator/conditional/AllOfConditional.java b/governator-core/src/main/java/com/netflix/governator/conditional/AllOfConditional.java new file mode 100644 index 00000000..187bb6aa --- /dev/null +++ b/governator-core/src/main/java/com/netflix/governator/conditional/AllOfConditional.java @@ -0,0 +1,27 @@ +package com.netflix.governator.conditional; + +import java.util.ArrayList; +import java.util.List; + +import com.google.inject.Injector; + +/** + * Conditional that is true if and only if all child conditionals are true + */ +public class AllOfConditional implements Conditional { + private final List children; + + public AllOfConditional(List children) { + this.children = new ArrayList<>(children); + } + + @Override + public boolean matches(Injector injector) { + for (Conditional conditional : children) { + if (!conditional.matches(injector)) { + return false; + } + } + return true; + } +} diff --git a/governator-core/src/main/java/com/netflix/governator/conditional/AnyOfConditional.java b/governator-core/src/main/java/com/netflix/governator/conditional/AnyOfConditional.java new file mode 100644 index 00000000..c2ea521c --- /dev/null +++ b/governator-core/src/main/java/com/netflix/governator/conditional/AnyOfConditional.java @@ -0,0 +1,28 @@ +package com.netflix.governator.conditional; + +import java.util.ArrayList; +import java.util.List; + +import com.google.inject.Injector; + +/** + * Conditional that is true if at least one of the child conditionals + * is true + */ +public class AnyOfConditional implements Conditional { + private final List children; + + public AnyOfConditional(List children) { + this.children = new ArrayList<>(children); + } + + @Override + public boolean matches(Injector injector) { + for (Conditional conditional : children) { + if (conditional.matches(injector)) { + return true; + } + } + return false; + } +} diff --git a/governator-core/src/main/java/com/netflix/governator/conditional/Conditional.java b/governator-core/src/main/java/com/netflix/governator/conditional/Conditional.java new file mode 100644 index 00000000..17a385ab --- /dev/null +++ b/governator-core/src/main/java/com/netflix/governator/conditional/Conditional.java @@ -0,0 +1,20 @@ +package com.netflix.governator.conditional; + +import com.google.inject.Injector; + +/** + * Contract for any conditional that may be applied to a conditional binding + * bound via {@link ConditionalBinder}. + * + * Dependencies needed by a concrete conditional should be injected using + * member injection. + * + */ +public interface Conditional { + /** + * Evaluate whether the condition is true. evaluate() is only called once at injector + * creation time. + * @return True if conditional is true otherwise false + */ + boolean matches(Injector injector); +} diff --git a/governator-core/src/main/java/com/netflix/governator/conditional/ConditionalBinder.java b/governator-core/src/main/java/com/netflix/governator/conditional/ConditionalBinder.java new file mode 100644 index 00000000..1d31f9ea --- /dev/null +++ b/governator-core/src/main/java/com/netflix/governator/conditional/ConditionalBinder.java @@ -0,0 +1,409 @@ +package com.netflix.governator.conditional; + +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Set; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.ConfigurationException; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Provider; +import com.google.inject.Stage; +import com.google.inject.TypeLiteral; +import com.google.inject.binder.LinkedBindingBuilder; +import com.google.inject.internal.Errors; +import com.google.inject.spi.BindingTargetVisitor; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.HasDependencies; +import com.google.inject.spi.Message; +import com.google.inject.spi.ProviderInstanceBinding; +import com.google.inject.spi.ProviderWithExtensionVisitor; +import com.google.inject.spi.Toolable; + +/** + * An API to bind multiple conditional candidates separately, only to evaluate and + * resolve to one candidate when the type is actually injected. + * ConditionalBinder is intended for use in a Module + * + *

+ * public class SnacksModule extends AbstractModule {
+ *   protected void configure() {
+ *     ConditionalBinder<Snack> conditionalbinder
+ *         = ConditionalBinder.newConditionalBinder(binder(), Snack.class);
+ *     conditionalbinder.whenMatchBind(new ConditionalOnProperty("type", "twix")).toInstance(new Twix());
+ *     conditionalbinder.whenMatchBind(new ConditionalOnProperty("type", "snickers")).toProvider(SnickersProvider.class);
+ *     conditionalbinder.whenMatchBind(new ConditionalOnProperty("type", "skittles")).to(Skittles.class);
+ *     conditionalbinder.whenNoMatchBind().to(Carrots.class);
+ *   }
+ * }
+ * + *

With this binding when injecting {@code } all conditionals will be evaluated and only + * the single matched binding will be injected + *


+ * class SnackMachine {
+ *   {@literal @}Inject
+ *   public SnackMachine(Snack snacks) { ... }
+ * }
+ * + *

Contributing conditional bindings from different modules is supported. For + * example, it is okay for both {@code CandyModule} and {@code ChipsModule} + * to create their own {@code ConditionalBinder}, and to each contribute + * conditional bindings to the set of candidate snacks. When a Snack is injected, it will + * use the one conditional bindings that matched. + * + * Exactly one conditional binding may be matched. An exception will be thrown + * when Snack is injected and no or multiple bindings' conditions are matched. + * + *

Conditionals are evaluated at injection time. If an element is bound to a + * provider, that provider's get method will be called each time Snack is + * injected (unless the binding is in Singleton scope, in which case Guice will cache + * the first call to get). + * + * Conditionals may only be used in Stage.DEVELOPMENT since running in Stage.PRODUCTION will + * result in every Singleton conditional candidate being instantiated eagerly. Any attempt + * to binding in Stage.PRODUCTION will result in a CreationException + * + * TODO: Strip all conditional bindings of their scope so they cannot be eager singletons. + * Alternatively, force lazy singleton behavior + * TODO: Discuss whether the conditional binder key should be singleton by default + */ +public abstract class ConditionalBinder { + /** + * Returns a new ConditionalBinder that tracks all candidate instances of {@code type}. + */ + public static ConditionalBinder newConditionalBinder(Binder binder, TypeLiteral type) { + return newRealConditionalBinder(binder, Key.get(type)); + } + + /** + * Returns a new ConditionalBinder that tracks all candidate instances of {@code type}. + */ + public static ConditionalBinder newConditionalBinder(Binder binder, Class type) { + return newRealConditionalBinder(binder, Key.get(type)); + } + + /** + * Returns a new ConditionalBinder that tracks all candidate instances of {@code type} with + * the qualifier {@code annotation} + */ + public static ConditionalBinder newConditionalBinder( + Binder binder, TypeLiteral type, Annotation annotation) { + return newRealConditionalBinder(binder, Key.get(type, annotation)); + } + + /** + * Returns a new ConditionalBinder that tracks all candidate instances of {@code type} with + * the qualifier {@code annotation} + */ + public static ConditionalBinder newConditionalBinder( + Binder binder, Class type, Annotation annotation) { + return newRealConditionalBinder(binder, Key.get(type, annotation)); + } + + /** + * Returns a new ConditionalBinder that tracks all candidate instances of {@code type} with + * the qualifier {@code annotation} + */ + public static ConditionalBinder newConditionalBinder(Binder binder, TypeLiteral type, + Class annotationType) { + return newRealConditionalBinder(binder, Key.get(type, annotationType)); + } + + /** + * Returns a new ConditionalBinder that tracks all candidate instances of {@code key} where + * key may or may not have a qualifier. + */ + public static ConditionalBinder newConditionalBinder(Binder binder, Key key) { + return newRealConditionalBinder(binder, key); + } + + /** + * Returns a new ConditionalBinder that tracks all candidate instances of {@code type} with + * the qualifier {@code annotation} + */ + public static ConditionalBinder newConditionalBinder(Binder binder, Class type, + Class annotationType) { + return newRealConditionalBinder(binder, Key.get(type, annotationType)); + } + + static ConditionalBinder newRealConditionalBinder(Binder binder, Key key) { + if (binder.currentStage().equals(Stage.PRODUCTION)) { + throw new RuntimeException("ConditionalBinder may not be used in Stage.PRODUCTION. Use Stage.DEVELOPMENT."); + } + + binder = binder.skipSources(RealConditionalBinder.class, ConditionalBinder.class); + RealConditionalBinder result = new RealConditionalBinder(binder, key); + binder.install(result); + return result; + } + + /** + * Returns a binding builder used to add a new conditional candidate for the key. + * Each bound element must have a distinct value. Only the matching candidate will + * be instantiated and its provider cached when the key is first injected. + * + *

It is an error to call this method without also calling one of the + * {@code to} methods on the returned binding builder. + * + *

Scoping elements independently supported per binding. Use the {@code in} method + * to specify a binding scope. + */ + public abstract LinkedBindingBuilder whenMatchBind(Conditional obj); + + /** + * Returns a binding builder used to specify the candidate for the key when no other + * conditional bindings have been met. There can be only one default candidate. Only the + * matching candidate will be instantiated and its provider cached when the key is + * first injected. + * + *

It is an error to call this method without also calling one of the + * {@code to} methods on the returned binding builder. + * + *

Scoping elements independently supported per binding. Use the {@code in} method + * to specify a binding scope. + */ + public abstract LinkedBindingBuilder whenNoMatchBind(); + + static final class RealConditionalBinder extends ConditionalBinder + implements Module, ProviderWithExtensionVisitor, ConditionalBinding, HasDependencies { + + private final TypeLiteral elementType; + private final String keyName; + private final Key primaryKey; + private ImmutableList> candidateBindings; + private Set> dependencies; + + private Provider matchedProvider; + private Binder binder; + + public RealConditionalBinder( + Binder binder, + Key key) { + this.binder = checkNotNull(binder, "binder"); + this.primaryKey = checkNotNull(key, "key"); + this.elementType = checkNotNull(key.getTypeLiteral(), "elementType"); + this.keyName = checkNotNull(ConditionalElementImpl.nameOf(key), "keyName"); + } + + @Override + public void configure(Binder binder) { + checkConfiguration(!isInitialized(), "ConditionalBinder was already initialized"); + binder.bind(primaryKey).toProvider(this); + } + + Key getKeyForNewItem(Conditional obj) { + checkConfiguration(!isInitialized(), "ConditionalBinder was already initialized"); + return Key.get(elementType, new ConditionalElementImpl(keyName, obj)); + } + + @Override + public LinkedBindingBuilder whenMatchBind(Conditional obj) { + return binder.bind(getKeyForNewItem(obj)); + } + + @Override + public LinkedBindingBuilder whenNoMatchBind() { + return binder.bind(getKeyForNewItem(null)); + } + + /** + * Invoked by Guice at Injector-creation time to prepare providers for each + * element in this set. + */ + @Toolable + @Inject + void initialize(final Injector injector) { + List> candidateBindings = Lists.newArrayList(); + Binding matchedBinding = null; + Binding defaultBinding = null; + + Set index = Sets.newHashSet(); + Indexer indexer = new Indexer(injector); + List> dependencies = Lists.newArrayList(); + + for (Binding entry : injector.findBindingsByType(elementType)) { + if (keyMatches(entry.getKey())) { + @SuppressWarnings("unchecked") // protected by findBindingsByType() + Binding binding = (Binding) entry; + if (index.add(binding.acceptTargetVisitor(indexer))) { + candidateBindings.add(binding); + + Conditional condition = ((ConditionalElementImpl) entry.getKey().getAnnotation()).getCondition(); + if (condition == null) { + if (defaultBinding == null) { + defaultBinding = binding; + } + else { + throw newDuplicateBindingException(primaryKey, defaultBinding, binding); + } + } + else { + if (condition.matches(injector)) { + if (matchedBinding == null) { + matchedBinding = binding; + } + else { + throw newDuplicateBindingException(primaryKey, matchedBinding, binding); + } + } + } + dependencies.add(Dependency.get(binding.getKey())); + } + } + } + + if (matchedBinding == null) { + matchedBinding = defaultBinding; + } + + if (matchedBinding == null) { + throw newNoMatchingBindingException(primaryKey, candidateBindings); + } + + dependencies.add(Dependency.get(matchedBinding.getKey())); + this.matchedProvider = matchedBinding.getProvider(); + + this.candidateBindings = ImmutableList.copyOf(candidateBindings); + this.binder = null; + } + + private boolean keyMatches(Key key) { + return key.getTypeLiteral().equals(elementType) + && key.getAnnotation() instanceof ConditionalElement + && ((ConditionalElement) key.getAnnotation()).keyName().equals(keyName); + } + + @Override + public T get() { + checkConfiguration(isInitialized(), "ConditionalBinder is not initialized"); + return matchedProvider.get(); + } + + @Override + public Key getKey() { + return primaryKey; + } + + @Override + public List> getCandidateElements() { + if (isInitialized()) { + return (List>) (List) candidateBindings; // safe because bindings is immutable. + } + else { + throw new UnsupportedOperationException("getElements() not supported for module bindings"); + } + } + + @Override + public V acceptExtensionVisitor( + BindingTargetVisitor visitor, + ProviderInstanceBinding binding) { + if (visitor instanceof ConditionalBindingsTargetVisitor) { + return ((ConditionalBindingsTargetVisitor) visitor).visit(this); + } + else { + return visitor.visit(binding); + } + } + + private boolean isInitialized() { + return binder == null; + } + + @Override + public boolean containsElement(com.google.inject.spi.Element element) { + if (element instanceof Binding) { + Binding binding = (Binding) element; + return keyMatches(binding.getKey()) + || binding.getKey().equals(primaryKey); + } + else { + return false; + } + } + + @Override + public Set> getDependencies() { + return dependencies; + } + + @Override + public int hashCode() { + return primaryKey.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + RealConditionalBinder other = (RealConditionalBinder) obj; + return primaryKey.equals(other.primaryKey); + } + } + + static void checkConfiguration(boolean condition, String format, Object... args) { + if (condition) { + return; + } + + throw new ConfigurationException(ImmutableSet.of(new Message(Errors.format(format, args)))); + } + + private static ConfigurationException newDuplicateBindingException( + Key key, + Binding existingBindings, + Binding duplicateBinding) { + // When the value strings don't match, include them both as they may be useful for debugging + return new ConfigurationException(ImmutableSet.of(new Message(Errors.format( + "%s injection failed due to multiple matching conditionals:" + + "\n \"%s\"\n bound at %s" + + "\n \"%s\"\n bound at %s", + key, + duplicateBinding.getKey(), + duplicateBinding.getSource(), + existingBindings.getKey(), + existingBindings.getSource())))); + } + + private static ConfigurationException newNoMatchingBindingException( + Key key, + List> candidateBindings) { + + StringBuilder builder = new StringBuilder(); + builder.append(String.format("%s injection failed due to no matching conditional", key)); + + if (!candidateBindings.isEmpty()) { + for (Binding binding : candidateBindings) { + builder.append(String.format("\n \"%s\"\n bound at %s", + binding.getKey(), + binding.getSource())); + } + } + else { + builder.append("\n No to() bindings were specified "); + } + return new ConfigurationException(ImmutableSet.of(new Message(Errors.format(builder.toString())))); + } + + static T checkNotNull(T reference, String name) { + if (reference != null) { + return reference; + } + + NullPointerException npe = new NullPointerException(name); + throw new ConfigurationException(ImmutableSet.of(new Message(npe + .toString(), npe))); + } +} diff --git a/governator-core/src/main/java/com/netflix/governator/conditional/ConditionalBinding.java b/governator-core/src/main/java/com/netflix/governator/conditional/ConditionalBinding.java new file mode 100644 index 00000000..a2244af0 --- /dev/null +++ b/governator-core/src/main/java/com/netflix/governator/conditional/ConditionalBinding.java @@ -0,0 +1,25 @@ +package com.netflix.governator.conditional; + +import java.util.List; + +import com.google.inject.Binding; +import com.google.inject.Key; +import com.google.inject.spi.Element; + +/** + * Binding object passed to a ConditionalBindingTargetVisitor when visiting + * Guice's bindings + */ +public interface ConditionalBinding { + /** + * @return Key for the main type for which there are conditional bindings + */ + Key getKey(); + + /** + * @return All candidate elements of which one should match + */ + List> getCandidateElements(); + + boolean containsElement(Element element); +} diff --git a/governator-core/src/main/java/com/netflix/governator/conditional/ConditionalBindingsTargetVisitor.java b/governator-core/src/main/java/com/netflix/governator/conditional/ConditionalBindingsTargetVisitor.java new file mode 100644 index 00000000..df4f06ec --- /dev/null +++ b/governator-core/src/main/java/com/netflix/governator/conditional/ConditionalBindingsTargetVisitor.java @@ -0,0 +1,7 @@ +package com.netflix.governator.conditional; + +import com.google.inject.spi.BindingTargetVisitor; + +public interface ConditionalBindingsTargetVisitor extends BindingTargetVisitor { + V visit(ConditionalBinding conditionalBinding); +} diff --git a/governator-core/src/main/java/com/netflix/governator/conditional/ConditionalElement.java b/governator-core/src/main/java/com/netflix/governator/conditional/ConditionalElement.java new file mode 100644 index 00000000..8150f2e5 --- /dev/null +++ b/governator-core/src/main/java/com/netflix/governator/conditional/ConditionalElement.java @@ -0,0 +1,19 @@ +package com.netflix.governator.conditional; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; + +import com.google.inject.BindingAnnotation; + +/** + * Unique qualifier associated with conditional bindings. The unique qualifier + * is used to ensure that the bound element isn't bound to a real key that user + * code would inject. + */ +@Retention(RUNTIME) +@BindingAnnotation +@interface ConditionalElement { + String keyName(); + int uniqueId(); +} diff --git a/governator-core/src/main/java/com/netflix/governator/conditional/ConditionalElementImpl.java b/governator-core/src/main/java/com/netflix/governator/conditional/ConditionalElementImpl.java new file mode 100644 index 00000000..2999eb22 --- /dev/null +++ b/governator-core/src/main/java/com/netflix/governator/conditional/ConditionalElementImpl.java @@ -0,0 +1,84 @@ +package com.netflix.governator.conditional; + +import java.lang.annotation.Annotation; +import java.util.concurrent.atomic.AtomicInteger; + +import com.google.inject.Key; +import com.google.inject.internal.Annotations; + +class ConditionalElementImpl implements ConditionalElement { + private static final AtomicInteger nextUniqueId = new AtomicInteger(1); + + private final int uniqueId; + private final String keyName; + private final Conditional condition; + + ConditionalElementImpl(String keyName, Conditional condition) { + this(keyName, condition, nextUniqueId.incrementAndGet()); + } + + ConditionalElementImpl(String keyName, Conditional condition, int uniqueId) { + this.uniqueId = uniqueId; + this.keyName = keyName; + this.condition = condition; + } + + @Override + public String keyName() { + return keyName; + } + + @Override + public int uniqueId() { + return uniqueId; + } + + public Conditional getCondition() { + return condition; + } + + @Override + public Class annotationType() { + return ConditionalElement.class; + } + + @Override + public String toString() { + return "@" + ConditionalElement.class.getName() + "(keyName=" + keyName + + ",uniqueId=" + uniqueId + ",condition=" + condition + ")"; + } + + @Override + public boolean equals(Object o) { + return o instanceof ConditionalElement + && ((ConditionalElement) o).keyName().equals(keyName()) + && ((ConditionalElement) o).uniqueId() == uniqueId(); + } + + @Override + public int hashCode() { + return ((127 * "keyName".hashCode()) ^ keyName.hashCode()) + + ((127 * "uniqueId".hashCode()) ^ uniqueId); + } + + /** + * Returns the name the binding should use. This is based on the annotation. + * If the annotation has an instance and is not a marker annotation, we ask + * the annotation for its toString. If it was a marker annotation or just an + * annotation type, we use the annotation's name. Otherwise, the name is the + * empty string. + */ + static String nameOf(Key key) { + Annotation annotation = key.getAnnotation(); + Class annotationType = key.getAnnotationType(); + if (annotation != null && !Annotations.isMarker(annotationType)) { + return key.getAnnotation().toString(); + } + else if (key.getAnnotationType() != null) { + return "@" + key.getAnnotationType().getName(); + } + else { + return ""; + } + } +} \ No newline at end of file diff --git a/governator-core/src/main/java/com/netflix/governator/conditional/ConditionalOnProfile.java b/governator-core/src/main/java/com/netflix/governator/conditional/ConditionalOnProfile.java new file mode 100644 index 00000000..9bac1a5c --- /dev/null +++ b/governator-core/src/main/java/com/netflix/governator/conditional/ConditionalOnProfile.java @@ -0,0 +1,33 @@ +package com.netflix.governator.conditional; + +import java.util.Set; + +import com.google.inject.Binding; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import com.netflix.governator.annotations.binding.Profiles; + +final public class ConditionalOnProfile implements Conditional { + private static final TypeLiteral> STRING_SET_TYPE = new TypeLiteral>() {}; + + private final String profile; + + public ConditionalOnProfile(String profile) { + this.profile = profile; + } + + @Override + public boolean matches(Injector injector) { + Binding> profiles = injector.getExistingBinding(Key.get(STRING_SET_TYPE, Profiles.class)); + if (profiles == null) { + return false; + } + return profiles.getProvider().get().contains(profile); + } + + @Override + public String toString() { + return "ConditionalOnProfile[" + profile + "]"; + } +} diff --git a/governator-core/src/main/java/com/netflix/governator/conditional/ConditionalOnProperty.java b/governator-core/src/main/java/com/netflix/governator/conditional/ConditionalOnProperty.java new file mode 100644 index 00000000..2d297bbe --- /dev/null +++ b/governator-core/src/main/java/com/netflix/governator/conditional/ConditionalOnProperty.java @@ -0,0 +1,33 @@ +package com.netflix.governator.conditional; + +import com.google.inject.Binding; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.netflix.governator.spi.PropertySource; + +/** + * Conditional that evaluates to true if the a property is set to a specific value + */ +public class ConditionalOnProperty implements Conditional { + private final String value; + private final String key; + + public ConditionalOnProperty(String key, String value) { + this.key = key; + this.value = value; + } + + @Override + public boolean matches(Injector injector) { + Binding properties = injector.getExistingBinding(Key.get(PropertySource.class)); + if (properties == null) { + return false; + } + return value.equals(properties.getProvider().get().get(key, "")); + } + + @Override + public String toString() { + return "ConditionalOnProperty[" + key + "=" + value + "]"; + } +} diff --git a/governator-core/src/main/java/com/netflix/governator/conditional/Conditionals.java b/governator-core/src/main/java/com/netflix/governator/conditional/Conditionals.java new file mode 100644 index 00000000..6c08a002 --- /dev/null +++ b/governator-core/src/main/java/com/netflix/governator/conditional/Conditionals.java @@ -0,0 +1,44 @@ +package com.netflix.governator.conditional; + +import java.util.Arrays; + +import com.google.common.base.Preconditions; + +/** + * Utility class for creating conditional + */ +public abstract class Conditionals { + private Conditionals() { + } + + /** + * Create a conditional that matches to true if and only if all of the child conditionals + * are true + * + * @param conditional + * @return + */ + public static Conditional allOf(Conditional... conditional) { + Preconditions.checkNotNull(conditional, "Cannot have a conditional allOf(null)"); + return new AllOfConditional(Arrays.asList(conditional)); + } + + /** + * Create a conditional that matches to true if at least one of the child conditionals is + * true + * @param conditional + * @return + */ + public static Conditional anyOf(Conditional... conditional) { + Preconditions.checkNotNull(conditional, "Cannot have a conditional anyOf(null)"); + return new AnyOfConditional(Arrays.asList(conditional)); + } + + /** + * @return Create a conditional that is a logical NOT of the provided conditional + */ + public static Conditional not(Conditional conditional) { + return new NotConditional(conditional); + } + +} diff --git a/governator-core/src/main/java/com/netflix/governator/conditional/Indexer.java b/governator-core/src/main/java/com/netflix/governator/conditional/Indexer.java new file mode 100644 index 00000000..f88ef259 --- /dev/null +++ b/governator-core/src/main/java/com/netflix/governator/conditional/Indexer.java @@ -0,0 +1,185 @@ +package com.netflix.governator.conditional; + +/** + * Copyright (C) 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.lang.annotation.Annotation; + +import com.google.common.base.Objects; +import com.google.inject.Binding; +import com.google.inject.Injector; +import com.google.inject.Scope; +import com.google.inject.Scopes; +import com.google.inject.TypeLiteral; +import com.google.inject.spi.BindingScopingVisitor; +import com.google.inject.spi.ConstructorBinding; +import com.google.inject.spi.ConvertedConstantBinding; +import com.google.inject.spi.DefaultBindingTargetVisitor; +import com.google.inject.spi.ExposedBinding; +import com.google.inject.spi.InstanceBinding; +import com.google.inject.spi.LinkedKeyBinding; +import com.google.inject.spi.ProviderBinding; +import com.google.inject.spi.ProviderInstanceBinding; +import com.google.inject.spi.ProviderKeyBinding; +import com.google.inject.spi.UntargettedBinding; + +/** + * Visits bindings to return a {@code IndexedBinding} that can be used to + * emulate the binding deduplication that Guice internally performs. + */ +class Indexer extends + DefaultBindingTargetVisitor implements + BindingScopingVisitor { + enum BindingType { + INSTANCE, PROVIDER_INSTANCE, PROVIDER_KEY, LINKED_KEY, UNTARGETTED, CONSTRUCTOR, CONSTANT, EXPOSED, PROVIDED_BY, + } + + static class IndexedBinding { + final String annotationName; + final TypeLiteral typeLiteral; + final Object scope; + final BindingType type; + final Object extraEquality; + + IndexedBinding(Binding binding, BindingType type, Object scope, + Object extraEquality) { + this.scope = scope; + this.type = type; + this.extraEquality = extraEquality; + this.typeLiteral = binding.getKey().getTypeLiteral(); + ConditionalElement annotation = (ConditionalElement) binding.getKey().getAnnotation(); + this.annotationName = annotation.keyName(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof IndexedBinding)) { + return false; + } + IndexedBinding o = (IndexedBinding) obj; + return type == o.type && Objects.equal(scope, o.scope) + && typeLiteral.equals(o.typeLiteral) + && annotationName.equals(o.annotationName) + && Objects.equal(extraEquality, o.extraEquality); + } + + @Override + public int hashCode() { + return Objects.hashCode(type, scope, typeLiteral, annotationName, + extraEquality); + } + } + + final Injector injector; + + Indexer(Injector injector) { + this.injector = injector; + } + + boolean isIndexable(Binding binding) { + return binding.getKey().getAnnotation() instanceof ConditionalElement; + } + + private Object scope(Binding binding) { + return binding.acceptScopingVisitor(this); + } + + @Override + public Indexer.IndexedBinding visit( + ConstructorBinding binding) { + return new Indexer.IndexedBinding(binding, BindingType.CONSTRUCTOR, + scope(binding), binding.getConstructor()); + } + + @Override + public Indexer.IndexedBinding visit( + ConvertedConstantBinding binding) { + return new Indexer.IndexedBinding(binding, BindingType.CONSTANT, + scope(binding), binding.getValue()); + } + + @Override + public Indexer.IndexedBinding visit(ExposedBinding binding) { + return new Indexer.IndexedBinding(binding, BindingType.EXPOSED, + scope(binding), binding); + } + + @Override + public Indexer.IndexedBinding visit( + InstanceBinding binding) { + return new Indexer.IndexedBinding(binding, BindingType.INSTANCE, + scope(binding), binding.getInstance()); + } + + @Override + public Indexer.IndexedBinding visit( + LinkedKeyBinding binding) { + return new Indexer.IndexedBinding(binding, BindingType.LINKED_KEY, + scope(binding), binding.getLinkedKey()); + } + + @Override + public Indexer.IndexedBinding visit( + ProviderBinding binding) { + return new Indexer.IndexedBinding(binding, BindingType.PROVIDED_BY, + scope(binding), injector.getBinding(binding.getProvidedKey())); + } + + @Override + public Indexer.IndexedBinding visit( + ProviderInstanceBinding binding) { + return new Indexer.IndexedBinding(binding, + BindingType.PROVIDER_INSTANCE, scope(binding), + binding.getUserSuppliedProvider()); + } + + @Override + public Indexer.IndexedBinding visit( + ProviderKeyBinding binding) { + return new Indexer.IndexedBinding(binding, BindingType.PROVIDER_KEY, + scope(binding), binding.getProviderKey()); + } + + @Override + public Indexer.IndexedBinding visit( + UntargettedBinding binding) { + return new Indexer.IndexedBinding(binding, BindingType.UNTARGETTED, + scope(binding), null); + } + + private static final Object EAGER_SINGLETON = new Object(); + + @Override + public Object visitEagerSingleton() { + return EAGER_SINGLETON; + } + + @Override + public Object visitNoScoping() { + return Scopes.NO_SCOPE; + } + + @Override + public Object visitScope(Scope scope) { + return scope; + } + + @Override + public Object visitScopeAnnotation( + Class scopeAnnotation) { + return scopeAnnotation; + } +} diff --git a/governator-core/src/main/java/com/netflix/governator/conditional/NotConditional.java b/governator-core/src/main/java/com/netflix/governator/conditional/NotConditional.java new file mode 100644 index 00000000..459fce98 --- /dev/null +++ b/governator-core/src/main/java/com/netflix/governator/conditional/NotConditional.java @@ -0,0 +1,21 @@ +package com.netflix.governator.conditional; + +import com.google.inject.Injector; + +/** + * Conditional equivalent to + * + * !conditional + */ +public class NotConditional implements Conditional { + private final Conditional conditional; + + public NotConditional(Conditional conditional) { + this.conditional = conditional; + } + + @Override + public boolean matches(Injector injector) { + return !conditional.matches(injector); + } +} diff --git a/governator-core/src/main/java/com/netflix/governator/internal/ElementsEx.java b/governator-core/src/main/java/com/netflix/governator/internal/ElementsEx.java index 6a06df53..5dc6649c 100644 --- a/governator-core/src/main/java/com/netflix/governator/internal/ElementsEx.java +++ b/governator-core/src/main/java/com/netflix/governator/internal/ElementsEx.java @@ -7,7 +7,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; import com.google.inject.Binding; import com.google.inject.ImplementedBy; diff --git a/governator-core/src/test/java/com/netflix/governator/conditional/ConditionalBinderTest.java b/governator-core/src/test/java/com/netflix/governator/conditional/ConditionalBinderTest.java new file mode 100644 index 00000000..a968cbe2 --- /dev/null +++ b/governator-core/src/test/java/com/netflix/governator/conditional/ConditionalBinderTest.java @@ -0,0 +1,346 @@ +package com.netflix.governator.conditional; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; + +import com.google.inject.AbstractModule; +import com.google.inject.CreationException; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Stage; +import com.google.inject.name.Names; +import com.google.inject.util.Modules; +import com.netflix.governator.PropertiesPropertySource; + +public class ConditionalBinderTest { + @Rule + public TestName name = new TestName(); + + public static interface Foo { + public String getName(); + } + + public static class FooImpl implements Foo { + private final String name; + + public FooImpl(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + } + + @Singleton + static class SingletonFoo implements Foo { + static int injectedCount = 0; + + @Inject + SingletonFoo() { + injectedCount++; + } + + @Override + public String getName() { + return "singleton"; + } + } + + static class NonSingletonFoo implements Foo { + static int injectedCount = 0; + + @Inject + NonSingletonFoo() { + injectedCount++; + } + + @Override + public String getName() { + return "nonsingleton"; + } + } + + @Before + public void before() { + SingletonFoo.injectedCount = 0; + NonSingletonFoo.injectedCount = 0; + } + + public static class ModuleGroup1 extends AbstractModule { + @Override + protected void configure() { + ConditionalBinder binder = ConditionalBinder.newConditionalBinder(binder(), Foo.class); + + binder.whenMatchBind(new ConditionalOnProperty("group1", "a")) + .toInstance(new FooImpl("group1_a")); + binder.whenMatchBind(new ConditionalOnProperty("group1", "b")) + .toInstance(new FooImpl("group1_b")); + binder.whenNoMatchBind() + .toInstance(new FooImpl("group1_default")); + } + } + + public static class ModuleGroup2 extends AbstractModule { + @Override + protected void configure() { + ConditionalBinder binder = ConditionalBinder.newConditionalBinder(binder(), Foo.class, Names.named("group2")); + + binder.whenMatchBind(new ConditionalOnProperty("group2", "a")) + .toInstance(new FooImpl("group2_a")); + binder.whenMatchBind(new ConditionalOnProperty("group2", "b")) + .toInstance(new FooImpl("group2_b")); + } + } + + public static class ModuleNoConditional extends AbstractModule { + @Override + protected void configure() { + bind(Key.get(Foo.class)).toInstance(new FooImpl("unconditional")); + } + } + + @Test + public void bindToMatchedOfMultipleConditionals() { + Injector injector = Guice.createInjector( + new PropertiesPropertySource() + .setProperty("group1", "a"), + new ModuleGroup1()); + + Foo foo1 = injector.getInstance(Key.get(Foo.class)); + Assert.assertEquals(foo1.getName(), "group1_a"); + } + + @Test + public void simpleWithDefault() { + Injector injector = Guice.createInjector( + new PropertiesPropertySource(), + new ModuleGroup1()); + + Foo foo1 = injector.getInstance(Key.get(Foo.class)); + Assert.assertEquals(foo1.getName(), "group1_default"); + } + + @Test + public void differentiateBetweenMultipleQualifiers() { + Injector injector = Guice.createInjector( + new PropertiesPropertySource() + .setProperty("group1", "a") + .setProperty("group2", "b"), + new ModuleGroup1(), + new ModuleGroup2()); + + Foo foo1 = injector.getInstance(Key.get(Foo.class)); + Assert.assertEquals(foo1.getName(), "group1_a"); + + Foo foo2 = injector.getInstance(Key.get(Foo.class, Names.named("group2"))); + Assert.assertEquals(foo2.getName(), "group2_b"); + } + + @Test + public void overrideConditionalWithNonConditional() { + Injector injector = Guice.createInjector(Modules.override( + new PropertiesPropertySource() + .setProperty("group1", "a"), + new ModuleGroup1() + ) + .with( + new ModuleNoConditional())); + + Foo foo = injector.getInstance(Key.get(Foo.class)); + Assert.assertEquals(foo.getName(), "unconditional"); + } + + @Test + public void bindToSingleConditionalWithNoDefault() { + Injector injector = Guice.createInjector( + new PropertiesPropertySource() + .setProperty("foo", "value"), + new AbstractModule() { + @Override + protected void configure() { + ConditionalBinder binder = ConditionalBinder.newConditionalBinder(binder(), Foo.class); + + binder.whenMatchBind(new ConditionalOnProperty("foo", "value")) + .toInstance(new FooImpl("value")); + } + }); + + Foo foo = injector.getInstance(Key.get(Foo.class)); + Assert.assertEquals(foo.getName(), "value"); + } + + @Test + public void bindToJustDefault() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + ConditionalBinder binder = ConditionalBinder.newConditionalBinder(binder(), Foo.class); + + binder.whenNoMatchBind() + .toInstance(new FooImpl("default")); + } + }); + + Foo foo = injector.getInstance(Key.get(Foo.class)); + Assert.assertEquals(foo.getName(), "default"); + } + + @Test + public void confirmSingletonConditionalBehavior() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + ConditionalBinder binder = ConditionalBinder.newConditionalBinder(binder(), Foo.class); + binder.whenNoMatchBind().to(SingletonFoo.class); + + bind(SingletonFoo.class); + } + }); + + Assert.assertEquals(0, SingletonFoo.injectedCount); + Foo foo1 = injector.getInstance(Foo.class); + Assert.assertEquals(1, SingletonFoo.injectedCount); + Foo foo2 = injector.getInstance(Foo.class); + Assert.assertEquals(1, SingletonFoo.injectedCount); + } + + @Test + public void confirmSingletonNotEagerlyCreated() { + Assert.assertEquals(0, SingletonFoo.injectedCount); + Assert.assertEquals(0, NonSingletonFoo.injectedCount); + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + ConditionalBinder binder = ConditionalBinder.newConditionalBinder(binder(), Foo.class); + binder.whenMatchBind(new ConditionalOnProperty("foo", "1")).to(SingletonFoo.class); + binder.whenNoMatchBind().to(NonSingletonFoo.class); + } + }); + + Assert.assertEquals(0, SingletonFoo.injectedCount); + Assert.assertEquals(0, NonSingletonFoo.injectedCount); + Foo foo1 = injector.getInstance(Foo.class); + Foo foo2 = injector.getInstance(Foo.class); + Assert.assertEquals(0, SingletonFoo.injectedCount); + Assert.assertEquals(2, NonSingletonFoo.injectedCount); + Assert.assertEquals(foo1.getName(), "nonsingleton"); + } + + @Test(expected=CreationException.class) + public void failWithNoMatchedConditionals(){ + try { + Guice.createInjector(new ModuleGroup1(), new ModuleGroup2()); + } + catch (Exception e) { + e.printStackTrace(); + throw e; + } + } + + @Test(expected=CreationException.class) + public void failWithNoBindTo(){ + try { + Guice.createInjector( + new AbstractModule() { + @Override + protected void configure() { + ConditionalBinder binder = ConditionalBinder.newConditionalBinder(binder(), Foo.class); + } + }); + } + catch (Exception e) { + e.printStackTrace(); + throw e; + } + } + + @Test(expected=CreationException.class) + public void failWithDuplicateMatchedConditionals(){ + try { + Guice.createInjector( + new PropertiesPropertySource() + .setProperty("group1", "a"), + new AbstractModule() { + @Override + protected void configure() { + ConditionalBinder binder = ConditionalBinder.newConditionalBinder(binder(), Foo.class); + + binder.whenMatchBind(new ConditionalOnProperty("group1", "a")) + .toInstance(new FooImpl("group1_a")); + binder.whenMatchBind(new ConditionalOnProperty("group1", "a")) + .toInstance(new FooImpl("group1_b")); + } + }); + } + catch (Exception e) { + e.printStackTrace(); + throw e; + } + } + + @Test(expected=CreationException.class) + public void failOnMultipleDefaults() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + ConditionalBinder binder = ConditionalBinder.newConditionalBinder(binder(), Foo.class); + + binder.whenNoMatchBind() + .toInstance(new FooImpl("default1")); + binder.whenNoMatchBind() + .toInstance(new FooImpl("default2")); + } + }); + } + catch (Exception e) { + e.printStackTrace(); + throw e; + } + } + + @Test(expected=CreationException.class) + public void conflictingConditionalAndNonConditional() { + try { + Guice.createInjector( + new PropertiesPropertySource() + .setProperty("group1", "a"), + new ModuleGroup1(), + new ModuleNoConditional()); + } + catch (Exception e) { + e.printStackTrace(); + throw e; + } + } + + @Test(expected=CreationException.class) + public void productionStageNotSupported() { + try { + Guice.createInjector(Stage.PRODUCTION, new AbstractModule() { + @Override + protected void configure() { + ConditionalBinder binder = ConditionalBinder.newConditionalBinder(binder(), Foo.class); + + binder.whenNoMatchBind().to(SingletonFoo.class); + binder.whenMatchBind(new ConditionalOnProperty("foo", "1")).to(NonSingletonFoo.class); + } + }); + } + catch (Exception e) { + e.printStackTrace(); + throw e; + } + + } +} diff --git a/governator-core/src/test/java/com/netflix/governator/conditional/ConditionalTest.java b/governator-core/src/test/java/com/netflix/governator/conditional/ConditionalTest.java new file mode 100644 index 00000000..a5811420 --- /dev/null +++ b/governator-core/src/test/java/com/netflix/governator/conditional/ConditionalTest.java @@ -0,0 +1,45 @@ +package com.netflix.governator.conditional; + +import junit.framework.Assert; + +import org.junit.Test; + +import com.google.inject.Injector; + +public class ConditionalTest { + public static class True implements Conditional { + @Override + public boolean matches(Injector injector) { + return true; + } + } + + public static class False implements Conditional { + @Override + public boolean matches(Injector injector) { + return false; + } + } + + @Test + public void testOr() { + Assert.assertTrue(Conditionals.anyOf(new True(), new True()).matches(null)); + Assert.assertTrue(Conditionals.anyOf(new True(), new False()).matches(null)); + Assert.assertTrue(Conditionals.anyOf(new False(), new True()).matches(null)); + Assert.assertFalse(Conditionals.anyOf(new False(), new False()).matches(null)); + } + + @Test + public void testAnd() { + Assert.assertTrue(Conditionals.allOf(new True(), new True()).matches(null)); + Assert.assertFalse(Conditionals.allOf(new True(), new False()).matches(null)); + Assert.assertFalse(Conditionals.allOf(new False(), new True()).matches(null)); + Assert.assertFalse(Conditionals.allOf(new False(), new False()).matches(null)); + } + + @Test + public void testNot() { + Assert.assertFalse(Conditionals.not(new True()).matches(null)); + Assert.assertTrue(Conditionals.not(new False()).matches(null)); + } +}