diff options
22 files changed, 1704 insertions, 13 deletions
@@ -6,7 +6,7 @@ <groupId>com.tavianator.sangria</groupId> <artifactId>sangria</artifactId> - <version>1.0</version> + <version>1.1-SNAPSHOT</version> <packaging>pom</packaging> <name>Sangria</name> @@ -51,25 +51,31 @@ <dependency> <groupId>com.tavianator.sangria</groupId> <artifactId>sangria-core</artifactId> - <version>1.0</version> + <version>1.1-SNAPSHOT</version> </dependency> <dependency> <groupId>com.tavianator.sangria</groupId> <artifactId>sangria-contextual</artifactId> - <version>1.0</version> + <version>1.1-SNAPSHOT</version> </dependency> <dependency> <groupId>com.tavianator.sangria</groupId> <artifactId>sangria-slf4j</artifactId> - <version>1.0</version> + <version>1.1-SNAPSHOT</version> </dependency> <dependency> <groupId>com.tavianator.sangria</groupId> <artifactId>sangria-log4j</artifactId> - <version>1.0</version> + <version>1.1-SNAPSHOT</version> + </dependency> + + <dependency> + <groupId>com.tavianator.sangria</groupId> + <artifactId>sangria-listbinder</artifactId> + <version>1.1-SNAPSHOT</version> </dependency> <dependency> @@ -193,6 +199,7 @@ <link>https://google-guice.googlecode.com/git/latest-javadoc/</link> <link>http://docs.guava-libraries.googlecode.com/git-history/v16.0.1/javadoc/</link> <link>http://slf4j.org/api/</link> + <link>http://logging.apache.org/log4j/2.x/log4j-api/apidocs/</link> <link>http://jsr-305.googlecode.com/svn/trunk/javadoc/</link> <link>http://junit.sourceforge.net/javadoc/</link> <link>http://hamcrest.org/JavaHamcrest/javadoc/1.3/</link> @@ -250,6 +257,7 @@ <link>https://google-guice.googlecode.com/git/latest-javadoc/</link> <link>http://docs.guava-libraries.googlecode.com/git-history/v16.0.1/javadoc/</link> <link>http://slf4j.org/api/</link> + <link>http://logging.apache.org/log4j/2.x/log4j-api/apidocs/</link> <link>http://jsr-305.googlecode.com/svn/trunk/javadoc/</link> <link>http://junit.sourceforge.net/javadoc/</link> <link>http://hamcrest.org/JavaHamcrest/javadoc/1.3/</link> @@ -279,6 +287,7 @@ <module>sangria-contextual</module> <module>sangria-slf4j</module> <module>sangria-log4j</module> + <module>sangria-listbinder</module> </modules> <profiles> diff --git a/sangria-contextual/pom.xml b/sangria-contextual/pom.xml index 02e928e..54d0a36 100644 --- a/sangria-contextual/pom.xml +++ b/sangria-contextual/pom.xml @@ -7,7 +7,7 @@ <parent> <groupId>com.tavianator.sangria</groupId> <artifactId>sangria</artifactId> - <version>1.0</version> + <version>1.1-SNAPSHOT</version> </parent> <artifactId>sangria-contextual</artifactId> diff --git a/sangria-core/pom.xml b/sangria-core/pom.xml index 59a7c31..8c58c49 100644 --- a/sangria-core/pom.xml +++ b/sangria-core/pom.xml @@ -7,7 +7,7 @@ <parent> <groupId>com.tavianator.sangria</groupId> <artifactId>sangria</artifactId> - <version>1.0</version> + <version>1.1-SNAPSHOT</version> </parent> <artifactId>sangria-core</artifactId> diff --git a/sangria-core/src/main/java/com/tavianator/sangria/core/DelayedError.java b/sangria-core/src/main/java/com/tavianator/sangria/core/DelayedError.java index d6f86a8..2aeabca 100644 --- a/sangria-core/src/main/java/com/tavianator/sangria/core/DelayedError.java +++ b/sangria-core/src/main/java/com/tavianator/sangria/core/DelayedError.java @@ -25,6 +25,8 @@ import com.google.inject.CreationException; import com.google.inject.Injector; import com.google.inject.spi.Message; +import static com.google.common.base.Preconditions.*; + /** * Similar to {@link Binder#addError(String, Object...)}, but can be canceled later. Useful for enforcing correct usage * of fluent APIs. @@ -35,6 +37,7 @@ import com.google.inject.spi.Message; */ public class DelayedError { private Throwable error; + private boolean reported = false; /** * Create a {@link DelayedError}. @@ -85,11 +88,13 @@ public class DelayedError { * Cancel this error. */ public void cancel() { - this.error = null; + checkState(!reported, "This error has already been reported"); + error = null; } @Inject - void inject(Injector injector) throws Throwable { + void reportErrors(Injector injector) throws Throwable { + reported = true; if (error != null) { throw error; } diff --git a/sangria-core/src/main/java/com/tavianator/sangria/core/PotentialAnnotation.java b/sangria-core/src/main/java/com/tavianator/sangria/core/PotentialAnnotation.java new file mode 100644 index 0000000..302e5e1 --- /dev/null +++ b/sangria-core/src/main/java/com/tavianator/sangria/core/PotentialAnnotation.java @@ -0,0 +1,241 @@ +/**************************************************************************** + * Sangria * + * Copyright (C) 2014 Tavian Barnes <tavianator@tavianator.com> * + * * + * 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. * + ****************************************************************************/ + +package com.tavianator.sangria.core; + +import java.lang.annotation.Annotation; +import java.util.*; + +import com.google.inject.CreationException; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import com.google.inject.spi.Message; + +/** + * A record of stored annotations, perfect for builders with {@code annotatedWith()} methods. + * + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.1 + * @since 1.1 + */ +public abstract class PotentialAnnotation { + /** + * A visitor interface to examine a {@link PotentialAnnotation}'s annotation, if it exists. + * + * @param <T> The type to return. + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.1 + * @since 1.1 + */ + public interface Visitor<T> { + /** + * Called when there is no annotation. + * + * @return Any value. + */ + T visitNoAnnotation(); + + /** + * Called when an annotation type is stored. + * + * @param annotationType The annotation type. + * @return Any value. + */ + T visitAnnotationType(Class<? extends Annotation> annotationType); + + /** + * Called when an annotation instance is stored. + * + * @param annotation The annotation instance. + * @return Any value. + */ + T visitAnnotationInstance(Annotation annotation); + } + + private static final PotentialAnnotation NONE = new NoAnnotation(); + + /** + * @return A {@link PotentialAnnotation} with no annotation. + */ + public static PotentialAnnotation none() { + return NONE; + } + + private PotentialAnnotation() { + } + + /** + * Add an annotation. + * + * @param annotationType The annotation type to add. + * @return A new {@link PotentialAnnotation} associated with the given annotation type. + * @throws CreationException If an annotation is already present. + */ + public PotentialAnnotation annotatedWith(Class<? extends Annotation> annotationType) { + throw annotationAlreadyPresent(); + } + + /** + * Add an annotation. + * + * @param annotation The annotation instance to add. + * @return A new {@link PotentialAnnotation} associated with the given annotation instance. + * @throws CreationException If an annotation is already present. + */ + public PotentialAnnotation annotatedWith(Annotation annotation) { + throw annotationAlreadyPresent(); + } + + private CreationException annotationAlreadyPresent() { + Message message = new Message("An annotation was already present"); + return new CreationException(Collections.singletonList(message)); + } + + /** + * @return Whether an annotation is present. + */ + public abstract boolean hasAnnotation(); + + /** + * Create a {@link Key} with the given type and the stored annotation. + * + * @param type The type of the key to create. + * @param <T> The type of the key to create. + * @return A {@link Key}. + */ + public <T> Key<T> getKey(Class<T> type) { + return getKey(TypeLiteral.get(type)); + } + + /** + * Create a {@link Key} with the given type and the stored annotation. + * + * @param type The type of the key to create. + * @param <T> The type of the key to create. + * @return A {@link Key}. + */ + public abstract <T> Key<T> getKey(TypeLiteral<T> type); + + /** + * Accept a {@link Visitor}. + * + * @param visitor The visitor to accept. + * @param <T> The type for the visitor to return. + * @return The value produced by the visitor. + */ + public abstract <T> T accept(Visitor<T> visitor); + + @Override + public abstract String toString(); + + /** + * Implementation of {@link #none()}. + */ + private static class NoAnnotation extends PotentialAnnotation { + @Override + public PotentialAnnotation annotatedWith(Class<? extends Annotation> annotationType) { + return new AnnotationType(annotationType); + } + + @Override + public PotentialAnnotation annotatedWith(Annotation annotation) { + return new AnnotationInstance(annotation); + } + + @Override + public boolean hasAnnotation() { + return false; + } + + @Override + public <T> Key<T> getKey(TypeLiteral<T> type) { + return Key.get(type); + } + + @Override + public <T> T accept(Visitor<T> visitor) { + return visitor.visitNoAnnotation(); + } + + @Override + public String toString() { + return "[no annotation]"; + } + } + + /** + * Implementation of {@link #annotatedWith(Class)}. + */ + private static class AnnotationType extends PotentialAnnotation { + private final Class<? extends Annotation> annotationType; + + AnnotationType(Class<? extends Annotation> annotationType) { + this.annotationType = annotationType; + } + + @Override + public boolean hasAnnotation() { + return true; + } + + @Override + public <T> Key<T> getKey(TypeLiteral<T> type) { + return Key.get(type, annotationType); + } + + @Override + public <T> T accept(Visitor<T> visitor) { + return visitor.visitAnnotationType(annotationType); + } + + @Override + public String toString() { + return "@" + annotationType.getCanonicalName(); + } + } + + /** + * Implementation of {@link #annotatedWith(Annotation)}. + */ + private static class AnnotationInstance extends PotentialAnnotation { + private final Annotation annotation; + + AnnotationInstance(Annotation annotation) { + this.annotation = annotation; + } + + @Override + public boolean hasAnnotation() { + return true; + } + + @Override + public <T> Key<T> getKey(TypeLiteral<T> type) { + return Key.get(type, annotation); + } + + @Override + public <T> T accept(Visitor<T> visitor) { + return visitor.visitAnnotationInstance(annotation); + } + + @Override + public String toString() { + return annotation.toString(); + } + } +} diff --git a/sangria-core/src/main/java/com/tavianator/sangria/core/Priority.java b/sangria-core/src/main/java/com/tavianator/sangria/core/Priority.java new file mode 100644 index 0000000..a833883 --- /dev/null +++ b/sangria-core/src/main/java/com/tavianator/sangria/core/Priority.java @@ -0,0 +1,148 @@ +/**************************************************************************** + * Sangria * + * Copyright (C) 2014 Tavian Barnes <tavianator@tavianator.com> * + * * + * 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. * + ****************************************************************************/ + +package com.tavianator.sangria.core; + +import java.util.*; + +import com.google.common.collect.ComparisonChain; +import com.google.common.primitives.Ints; + +/** + * A loosely-coupled, infinitely divisible priority/weight system. + * + * <p> + * This class implements an extensible priority system based on lexicographical ordering. In its simplest use, {@code + * Priority.create(0)} is ordered before {@code Priority.create(1)}, then {@code Priority.create(2)}, etc. + * </p> + * + * <p> + * To create a priority that is ordered between two existing ones, simply add another parameter: {@code + * Priority.create(1, 1)} comes after {@code Priority.create(1)}, but before {@code Priority.create(2)}. In this way, + * priorities can always be inserted anywhere in a sequence. + * </p> + * + * <p> + * The {@link #next()} method creates a priority that is ordered immediately following the current one, and is distinct + * from all priorities obtained by any other means. This provides a convenient way to order entire segments of lists. + * </p> + * + * <p> + * A special priority, obtained by {@code Priority.getDefault()}, sorts before all other priorities. + * </p> + * + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.1 + * @since 1.1 + */ +public class Priority implements Comparable<Priority> { + private static final Priority DEFAULT = new Priority(new int[0], 0); + private static final Comparator<int[]> COMPARATOR = Ints.lexicographicalComparator(); + + private final int[] weights; + private final int seq; + + /** + * @return The default priority, which comes before all other priorities. + */ + public static Priority getDefault() { + return DEFAULT; + } + + /** + * Create a {@link Priority} with the given sequence. + * + * @param weight The first value of the weight sequence. + * @param weights An integer sequence. These sequences are sorted lexicographically, so {@code Priority.create(1)} + * sorts before {@code Priority.create(1, 1)}, which sorts before {@code Priority.create(2)}. + * @return A new {@link Priority}. + */ + public static Priority create(int weight, int... weights) { + int[] newWeights = new int[weights.length + 1]; + newWeights[0] = weight; + System.arraycopy(weights, 0, newWeights, 1, weights.length); + return new Priority(newWeights, 0); + } + + private Priority(int[] weights, int seq) { + this.weights = weights; + this.seq = seq; + } + + /** + * @return Whether this priority originated in a call to {@link #getDefault()}. + */ + public boolean isDefault() { + return weights.length == 0; + } + + /** + * @return A new {@link Priority} which immediately follows this one, and which is distinct from all other + * priorities obtained by {@link #create(int, int...)}. + */ + public Priority next() { + return new Priority(weights, seq + 1); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } else if (!(obj instanceof Priority)) { + return false; + } + + Priority other = (Priority)obj; + return Arrays.equals(weights, other.weights) + && seq == other.seq; + } + + @Override + public int hashCode() { + return Arrays.hashCode(weights) + seq; + } + + @Override + public int compareTo(Priority o) { + return ComparisonChain.start() + .compare(weights, o.weights, COMPARATOR) + .compare(seq, o.seq) + .result(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (weights.length == 0) { + builder.append("default priority"); + } else { + builder.append("priority ["); + for (int i = 0; i < weights.length; ++i) { + if (i != 0) { + builder.append(", "); + } + builder.append(weights[i]); + } + builder.append("]"); + } + if (seq != 0) { + builder.append(" + ") + .append(seq); + } + return builder.toString(); + } +} diff --git a/sangria-core/src/main/java/com/tavianator/sangria/core/TypeLiterals.java b/sangria-core/src/main/java/com/tavianator/sangria/core/TypeLiterals.java new file mode 100644 index 0000000..ff42790 --- /dev/null +++ b/sangria-core/src/main/java/com/tavianator/sangria/core/TypeLiterals.java @@ -0,0 +1,90 @@ +/**************************************************************************** + * Sangria * + * Copyright (C) 2014 Tavian Barnes <tavianator@tavianator.com> * + * * + * 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. * + ****************************************************************************/ + +package com.tavianator.sangria.core; + +import java.util.*; + +import javax.inject.Provider; + +import com.google.inject.TypeLiteral; +import com.google.inject.util.Types; + +/** + * Static utility functions for working with {@link TypeLiteral}s. + * + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.1 + * @since 1.1 + */ +public class TypeLiterals { + private TypeLiterals() { + // Not for instantiating + } + + @SuppressWarnings("unchecked") + public static <T> TypeLiteral<List<T>> listOf(Class<T> type) { + return (TypeLiteral<List<T>>)TypeLiteral.get(Types.listOf(type)); + } + + @SuppressWarnings("unchecked") + public static <T> TypeLiteral<List<T>> listOf(TypeLiteral<T> type) { + return (TypeLiteral<List<T>>)TypeLiteral.get(Types.listOf(type.getType())); + } + + @SuppressWarnings("unchecked") + public static <T> TypeLiteral<Set<T>> setOf(Class<T> type) { + return (TypeLiteral<Set<T>>)TypeLiteral.get(Types.setOf(type)); + } + + @SuppressWarnings("unchecked") + public static <T> TypeLiteral<Set<T>> setOf(TypeLiteral<T> type) { + return (TypeLiteral<Set<T>>)TypeLiteral.get(Types.setOf(type.getType())); + } + + @SuppressWarnings("unchecked") + public static <K, V> TypeLiteral<Map<K, V>> mapOf(Class<K> keyType, Class<V> valueType) { + return (TypeLiteral<Map<K, V>>)TypeLiteral.get(Types.mapOf(keyType, valueType)); + } + + @SuppressWarnings("unchecked") + public static <K, V> TypeLiteral<Map<K, V>> mapOf(Class<K> keyType, TypeLiteral<V> valueType) { + return (TypeLiteral<Map<K, V>>)TypeLiteral.get(Types.mapOf(keyType, valueType.getType())); + } + + @SuppressWarnings("unchecked") + public static <K, V> TypeLiteral<Map<K, V>> mapOf(TypeLiteral<K> keyType, Class<V> valueType) { + return (TypeLiteral<Map<K, V>>)TypeLiteral.get(Types.mapOf(keyType.getType(), valueType)); + } + + @SuppressWarnings("unchecked") + public static <K, V> TypeLiteral<Map<K, V>> mapOf(TypeLiteral<K> keyType, TypeLiteral<V> valueType) { + return (TypeLiteral<Map<K, V>>)TypeLiteral.get(Types.mapOf(keyType.getType(), valueType.getType())); + } + + @SuppressWarnings("unchecked") + public static <T> TypeLiteral<Provider<T>> providerOf(Class<T> type) { + // Can't use Types.providerOf() because we want to stick to JSR-330 Providers + return (TypeLiteral<Provider<T>>)TypeLiteral.get(Types.newParameterizedType(Provider.class, type)); + } + + @SuppressWarnings("unchecked") + public static <T> TypeLiteral<Provider<T>> providerOf(TypeLiteral<T> type) { + // Can't use Types.providerOf() because we want to stick to JSR-330 Providers + return (TypeLiteral<Provider<T>>)TypeLiteral.get(Types.newParameterizedType(Provider.class, type.getType())); + } +} diff --git a/sangria-core/src/main/java/com/tavianator/sangria/core/package-info.java b/sangria-core/src/main/java/com/tavianator/sangria/core/package-info.java index 6d416d3..009aa5e 100644 --- a/sangria-core/src/main/java/com/tavianator/sangria/core/package-info.java +++ b/sangria-core/src/main/java/com/tavianator/sangria/core/package-info.java @@ -19,7 +19,7 @@ * {@code sangria-core}: Common code for Sangria. * * @author Tavian Barnes (tavianator@tavianator.com) - * @version 1.0 + * @version 1.1 * @since 1.0 */ package com.tavianator.sangria.core; diff --git a/sangria-core/src/test/java/com/tavianator/sangria/core/DelayedErrorTest.java b/sangria-core/src/test/java/com/tavianator/sangria/core/DelayedErrorTest.java index 349dced..f27aea8 100644 --- a/sangria-core/src/test/java/com/tavianator/sangria/core/DelayedErrorTest.java +++ b/sangria-core/src/test/java/com/tavianator/sangria/core/DelayedErrorTest.java @@ -32,7 +32,7 @@ import static org.hamcrest.Matchers.*; * Tests for {@link DelayedError}. * * @author Tavian Barnes (tavianator@tavianator.com) - * @version 1.0 + * @version 1.1 * @since 1.0 */ public class DelayedErrorTest { @@ -83,4 +83,31 @@ public class DelayedErrorTest { } }); } + + @Test + public void testCancel() { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + DelayedError error = DelayedError.create(binder(), "Message"); + error.cancel(); + } + }); + } + + @Test + public void testLateCancel() { + final DelayedError[] errorHolder = new DelayedError[1]; + + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + errorHolder[0] = DelayedError.create(binder(), "Message"); + errorHolder[0].cancel(); + } + }); + + thrown.expect(IllegalStateException.class); + errorHolder[0].cancel(); + } } diff --git a/sangria-core/src/test/java/com/tavianator/sangria/core/PotentialAnnotationTest.java b/sangria-core/src/test/java/com/tavianator/sangria/core/PotentialAnnotationTest.java new file mode 100644 index 0000000..c4ccc36 --- /dev/null +++ b/sangria-core/src/test/java/com/tavianator/sangria/core/PotentialAnnotationTest.java @@ -0,0 +1,126 @@ +/**************************************************************************** + * Sangria * + * Copyright (C) 2014 Tavian Barnes <tavianator@tavianator.com> * + * * + * 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. * + ****************************************************************************/ + +package com.tavianator.sangria.core; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import javax.inject.Qualifier; + +import com.google.inject.CreationException; +import com.google.inject.Key; +import com.google.inject.name.Names; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; +import org.junit.Test; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +/** + * Tests for {@link PotentialAnnotation}s. + * + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.1 + * @since 1.1 + */ +public class PotentialAnnotationTest { + @Retention(RetentionPolicy.RUNTIME) + @Qualifier + private @interface Simple { + } + + private final PotentialAnnotation none = PotentialAnnotation.none(); + private final Annotation nameAnnotation = Names.named("name"); + + @Test + public void testHasAnnotation() { + assertThat(none.hasAnnotation(), is(false)); + assertThat(none.annotatedWith(Simple.class).hasAnnotation(), is(true)); + assertThat(none.annotatedWith(nameAnnotation).hasAnnotation(), is(true)); + } + + @Test(expected = CreationException.class) + public void testInvalidAnnotatedWithType() { + none.annotatedWith(Simple.class) + .annotatedWith(Simple.class); + } + + @Test(expected = CreationException.class) + public void testInvalidAnnotatedWithInstance() { + none.annotatedWith(Names.named("name")) + .annotatedWith(Names.named("name")); + } + + @Test + public void testGetKey() { + assertThat(none.getKey(String.class), + equalTo(new Key<String>() { })); + assertThat(none.annotatedWith(Simple.class).getKey(String.class), + equalTo(new Key<String>(Simple.class) { })); + assertThat(none.annotatedWith(nameAnnotation).getKey(String.class), + equalTo(new Key<String>(nameAnnotation) { })); + } + + @Test + public void testVisitor() { + PotentialAnnotation.Visitor<String> visitor = new PotentialAnnotation.Visitor<String>() { + @Override + public String visitNoAnnotation() { + return "none"; + } + + @Override + public String visitAnnotationType(Class<? extends Annotation> annotationType) { + assertThat((Object)annotationType, equalTo((Object)Simple.class)); + return "type"; + } + + @Override + public String visitAnnotationInstance(Annotation annotation) { + assertThat(annotation, equalTo(nameAnnotation)); + return "instance"; + } + }; + + assertThat(none.accept(visitor), equalTo("none")); + assertThat(none.annotatedWith(Simple.class).accept(visitor), equalTo("type")); + assertThat(none.annotatedWith(nameAnnotation).accept(visitor), equalTo("instance")); + } + + @Test + public void testToString() { + assertThat(none.toString(), + equalTo("[no annotation]")); + assertThat(none.annotatedWith(Simple.class).toString(), + equalTo("@com.tavianator.sangria.core.PotentialAnnotationTest.Simple")); + assertThat(none.annotatedWith(nameAnnotation).toString(), + equalTo("@com.google.inject.name.Named(value=name)")); + } + + /** + * Needed to avoid compilation error to to inferred type being anonymous class. + */ + private static <T> Matcher<Key<T>> equalTo(Key<T> key) { + return Matchers.equalTo(key); + } + + private static <T> Matcher<T> equalTo(T object) { + return Matchers.equalTo(object); + } +} diff --git a/sangria-core/src/test/java/com/tavianator/sangria/core/PriorityTest.java b/sangria-core/src/test/java/com/tavianator/sangria/core/PriorityTest.java new file mode 100644 index 0000000..0869565 --- /dev/null +++ b/sangria-core/src/test/java/com/tavianator/sangria/core/PriorityTest.java @@ -0,0 +1,100 @@ +/**************************************************************************** + * Sangria * + * Copyright (C) 2014 Tavian Barnes <tavianator@tavianator.com> * + * * + * 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. * + ****************************************************************************/ + +package com.tavianator.sangria.core; + +import java.util.*; + +import org.junit.Test; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +/** + * Tests for {@link Priority}s. + * + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.1 + * @since 1.1 + */ +public class PriorityTest { + private final Priority defaultPriority = Priority.getDefault(); + private final Priority one = Priority.create(1); + private final Priority oneTwo = Priority.create(1, 2); + private final Priority two = Priority.create(2); + + @Test + public void testOrdering() { + List<Priority> list = Arrays.asList( + defaultPriority.next(), + two, + oneTwo.next(), + oneTwo, + two.next(), + defaultPriority, + defaultPriority.next().next(), + one, + one.next()); + Collections.sort(list); + assertThat(list, contains( + defaultPriority, + defaultPriority.next(), + defaultPriority.next().next(), + one, + one.next(), + oneTwo, + oneTwo.next(), + two, + two.next())); + + assertThat(defaultPriority, equalTo(Priority.getDefault())); + assertThat(defaultPriority.next(), equalTo(Priority.getDefault().next())); + assertThat(defaultPriority, not(equalTo(Priority.getDefault().next()))); + + assertThat(one, equalTo(Priority.create(1))); + assertThat(oneTwo, equalTo(Priority.create(1, 2))); + assertThat(two, equalTo(Priority.create(2))); + + assertThat(oneTwo.hashCode(), equalTo(Priority.create(1, 2).hashCode())); + } + + @Test + public void testIsDefault() { + assertThat(defaultPriority.isDefault(), is(true)); + assertThat(defaultPriority.next().isDefault(), is(true)); + + assertThat(one.isDefault(), is(false)); + assertThat(oneTwo.isDefault(), is(false)); + assertThat(two.isDefault(), is(false)); + assertThat(two.next().isDefault(), is(false)); + } + + @Test + public void testToString() { + assertThat(Priority.getDefault().toString(), equalTo("default priority")); + assertThat(Priority.getDefault().next().toString(), equalTo("default priority + 1")); + assertThat(Priority.getDefault().next().next().toString(), equalTo("default priority + 2")); + + assertThat(Priority.create(1).toString(), equalTo("priority [1]")); + assertThat(Priority.create(1).next().toString(), equalTo("priority [1] + 1")); + assertThat(Priority.create(1).next().next().toString(), equalTo("priority [1] + 2")); + + assertThat(Priority.create(1, 2).toString(), equalTo("priority [1, 2]")); + assertThat(Priority.create(1, 2).next().toString(), equalTo("priority [1, 2] + 1")); + assertThat(Priority.create(1, 2).next().next().toString(), equalTo("priority [1, 2] + 2")); + } +} diff --git a/sangria-core/src/test/java/com/tavianator/sangria/core/TypeLiteralsTest.java b/sangria-core/src/test/java/com/tavianator/sangria/core/TypeLiteralsTest.java new file mode 100644 index 0000000..3e7cdb8 --- /dev/null +++ b/sangria-core/src/test/java/com/tavianator/sangria/core/TypeLiteralsTest.java @@ -0,0 +1,81 @@ +/**************************************************************************** + * Sangria * + * Copyright (C) 2014 Tavian Barnes <tavianator@tavianator.com> * + * * + * 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. * + ****************************************************************************/ + +package com.tavianator.sangria.core; + +import java.util.*; + +import javax.inject.Provider; + +import com.google.inject.TypeLiteral; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Tests for {@link TypeLiterals}. + * + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.1 + * @since 1.1 + */ +public class TypeLiteralsTest { + @Test + public void testListOf() { + assertThat(TypeLiterals.listOf(String.class), + equalTo(new TypeLiteral<List<String>>() { })); + assertThat(TypeLiterals.listOf(new TypeLiteral<Class<?>>() { }), + equalTo(new TypeLiteral<List<Class<?>>>() { })); + } + + @Test + public void testSetOf() { + assertThat(TypeLiterals.setOf(String.class), + equalTo(new TypeLiteral<Set<String>>() { })); + assertThat(TypeLiterals.setOf(new TypeLiteral<Class<?>>() { }), + equalTo(new TypeLiteral<Set<Class<?>>>() { })); + } + + @Test + public void testMapOf() { + assertThat(TypeLiterals.mapOf(String.class, String.class), + equalTo(new TypeLiteral<Map<String, String>>() { })); + assertThat(TypeLiterals.mapOf(String.class, new TypeLiteral<Class<?>>() { }), + equalTo(new TypeLiteral<Map<String, Class<?>>>() { })); + assertThat(TypeLiterals.mapOf(new TypeLiteral<Class<?>>() { }, String.class), + equalTo(new TypeLiteral<Map<Class<?>, String>>() { })); + assertThat(TypeLiterals.mapOf(new TypeLiteral<Class<?>>() { }, new TypeLiteral<Class<?>>() { }), + equalTo(new TypeLiteral<Map<Class<?>, Class<?>>>() { })); + } + + @Test + public void testProviderOf() { + assertThat(TypeLiterals.providerOf(String.class), + equalTo(new TypeLiteral<Provider<String>>() { })); + assertThat(TypeLiterals.providerOf(new TypeLiteral<Class<?>>() { }), + equalTo(new TypeLiteral<Provider<Class<?>>>() { })); + } + + /** + * Needed to avoid compilation error to to inferred type being anonymous class. + */ + private static <T> Matcher<TypeLiteral<T>> equalTo(TypeLiteral<T> type) { + return Matchers.equalTo(type); + } +} diff --git a/sangria-listbinder/pom.xml b/sangria-listbinder/pom.xml new file mode 100644 index 0000000..5b04e5e --- /dev/null +++ b/sangria-listbinder/pom.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>com.tavianator.sangria</groupId> + <artifactId>sangria</artifactId> + <version>1.1-SNAPSHOT</version> + </parent> + + <artifactId>sangria-listbinder</artifactId> + <packaging>jar</packaging> + <name>Sangria ListBinder</name> + <description>A multi-binder with ordering guarantees</description> + + <dependencies> + <dependency> + <groupId>com.tavianator.sangria</groupId> + <artifactId>sangria-core</artifactId> + </dependency> + + <dependency> + <groupId>com.google.inject</groupId> + <artifactId>guice</artifactId> + </dependency> + + <dependency> + <groupId>com.google.inject.extensions</groupId> + <artifactId>guice-multibindings</artifactId> + </dependency> + + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + </dependency> + + <dependency> + <groupId>com.google.code.findbugs</groupId> + <artifactId>jsr305</artifactId> + <optional>true</optional> + </dependency> + + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest-integration</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <scope>test</scope> + </dependency> + </dependencies> +</project> diff --git a/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/AnnotatedListBinderBuilder.java b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/AnnotatedListBinderBuilder.java new file mode 100644 index 0000000..594971c --- /dev/null +++ b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/AnnotatedListBinderBuilder.java @@ -0,0 +1,28 @@ +package com.tavianator.sangria.listbinder; + +import java.lang.annotation.Annotation; + +/** + * Fluent builder interface. + * + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.1 + * @since 1.1 + */ +public interface AnnotatedListBinderBuilder<T> extends ListBinderBuilder<T> { + /** + * Make a binder for an annotated list type. + * + * @param annotationType The annotation type for the list. + * @return A fluent builder. + */ + ListBinderBuilder<T> annotatedWith(Class<? extends Annotation> annotationType); + + /** + * Make a binder for an annotated list type. + * + * @param annotation The annotation instance for the list. + * @return A fluent builder. + */ + ListBinderBuilder<T> annotatedWith(Annotation annotation); +} diff --git a/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinder.java b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinder.java new file mode 100644 index 0000000..d813b7a --- /dev/null +++ b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinder.java @@ -0,0 +1,427 @@ +/**************************************************************************** + * Sangria * + * Copyright (C) 2014 Tavian Barnes <tavianator@tavianator.com> * + * * + * 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. * + ****************************************************************************/ + +package com.tavianator.sangria.listbinder; + +import java.lang.annotation.Annotation; +import java.util.*; +import javax.inject.Inject; +import javax.inject.Provider; + +import com.google.common.base.Function; +import com.google.common.base.Objects; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ListMultimap; +import com.google.inject.AbstractModule; +import com.google.inject.Binder; +import com.google.inject.CreationException; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import com.google.inject.binder.LinkedBindingBuilder; +import com.google.inject.multibindings.Multibinder; +import com.google.inject.spi.Message; +import com.google.inject.util.Types; + +import com.tavianator.sangria.core.PotentialAnnotation; +import com.tavianator.sangria.core.PrettyTypes; +import com.tavianator.sangria.core.Priority; +import com.tavianator.sangria.core.TypeLiterals; +import com.tavianator.sangria.core.UniqueAnnotations; + +/** + * A multi-binder with guaranteed order. + * + * <p> + * {@link ListBinder} is much like {@link Multibinder}, except it provides a guaranteed iteration order, and binds a + * {@link List} instead of a {@link Set}. For example: + * </p> + * + * <pre> + * ListBinder<String> listBinder = ListBinder.build(binder(), String.class) + * .withDefaultPriority(); + * listBinder.addBinding().toInstance("a"); + * listBinder.addBinding().toInstance("b"); + * </pre> + * + * <p> + * This will create a binding for a {@code List<String>}, which contains {@code "a"} followed by {@code "b"}. It + * also creates a binding for {@code List<Provider<String>>} — this may be useful in more advanced + * cases to allow list elements to be lazily loaded. + * </p> + * + * <p>To add an annotation to the list binding, simply write this:</p> + * + * <pre> + * ListBinder<String> listBinder = ListBinder.build(binder(), String.class) + * .annotatedWith(Names.named("name")) + * .withDefaultPriority(); + * </pre> + * + * <p> + * and the created binding will be {@code @Named("name") List<String>} instead. + * </p> + * + * <p> + * For large lists, it may be helpful to split up their specification across different modules. This is accomplished by + * specifying <em>priorities</em> for the {@link ListBinder}s when they are created. For example: + * </p> + * + * <pre> + * // In some module + * ListBinder<String> listBinder1 = ListBinder.build(binder(), String.class) + * .withPriority(0); + * listBinder1.addBinding().toInstance("a"); + * listBinder1.addBinding().toInstance("b"); + * + * // ... some other module + * ListBinder<String> listBinder2 = ListBinder.build(binder(), String.class) + * .withPriority(1); + * listBinder2.addBinding().toInstance("c"); + * listBinder2.addBinding().toInstance("d"); + * </pre> + * + * <p> + * The generated list will contain {@code "a"}, {@code "b"}, {@code "c"}, {@code "d"}, in order. This happens because + * the first {@link ListBinder} had a smaller priority, so its entries come first. For more information about the + * priority system, see {@link Priority}. + * </p> + * + * @param <T> The type of the list element. + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.1 + * @since 1.1 + */ +public class ListBinder<T> { + private static final Class<?>[] SKIPPED_SOURCES = { + ListBinder.class, + BuilderImpl.class, + }; + + private final Binder binder; + private final Multibinder<ListElement<T>> multibinder; + private final Multibinder<ListBinderErrors<T>> errorMultibinder; + private final TypeLiteral<T> entryType; + private final Key<List<T>> listKey; + private final Key<List<Provider<T>>> listOfProvidersKey; + private final Key<Set<ListElement<T>>> setKey; + private final Key<Set<ListBinderErrors<T>>> errorSetKey; + private final PotentialAnnotation potentialAnnotation; + private final Priority initialPriority; + private Priority priority; + + private ListBinder( + Binder binder, + TypeLiteral<T> entryType, + PotentialAnnotation potentialAnnotation, + Priority initialPriority) { + this.binder = binder; + this.entryType = entryType; + + TypeLiteral<ListElement<T>> elementType = listElementOf(entryType); + TypeLiteral<ListBinderErrors<T>> errorsType = listBinderErrorsOf(entryType); + this.listKey = potentialAnnotation.getKey(TypeLiterals.listOf(entryType)); + this.listOfProvidersKey = potentialAnnotation.getKey(TypeLiterals.listOf(TypeLiterals.providerOf(entryType))); + this.setKey = potentialAnnotation.getKey(TypeLiterals.setOf(elementType)); + this.errorSetKey = potentialAnnotation.getKey(TypeLiterals.setOf(errorsType)); + this.multibinder = potentialAnnotation.accept(new MultibinderMaker<>(binder, elementType)); + this.errorMultibinder = potentialAnnotation.accept(new MultibinderMaker<>(binder, errorsType)); + + this.potentialAnnotation = potentialAnnotation; + this.priority = this.initialPriority = initialPriority; + } + + @SuppressWarnings("unchecked") + private static <T> TypeLiteral<ListElement<T>> listElementOf(TypeLiteral<T> type) { + return (TypeLiteral<ListElement<T>>)TypeLiteral.get(Types.newParameterizedType(ListElement.class, type.getType())); + } + + @SuppressWarnings("unchecked") + private static <T> TypeLiteral<ListBinderErrors<T>> listBinderErrorsOf(TypeLiteral<T> type) { + return (TypeLiteral<ListBinderErrors<T>>)TypeLiteral.get(Types.newParameterizedType(ListBinderErrors.class, type.getType())); + } + + /** + * {@link PotentialAnnotation.Visitor} that makes {@link Multibinder}s with the given annotation. + */ + private static class MultibinderMaker<T> implements PotentialAnnotation.Visitor<Multibinder<T>> { + private final Binder binder; + private final TypeLiteral<T> type; + + MultibinderMaker(Binder binder, TypeLiteral<T> type) { + this.binder = binder; + this.type = type; + } + + @Override + public Multibinder<T> visitNoAnnotation() { + return Multibinder.newSetBinder(binder, type); + } + + @Override + public Multibinder<T> visitAnnotationType(Class<? extends Annotation> annotationType) { + return Multibinder.newSetBinder(binder, type, annotationType); + } + + @Override + public Multibinder<T> visitAnnotationInstance(Annotation annotation) { + return Multibinder.newSetBinder(binder, type, annotation); + } + } + + /** + * Start building a {@link ListBinder}. + * + * @param binder The current binder, usually {@link AbstractModule#binder()}. + * @param type The type of the list element. + * @param <T> The type of the list element. + * @return A fluent builder. + */ + public static <T> AnnotatedListBinderBuilder<T> build(Binder binder, Class<T> type) { + return build(binder, TypeLiteral.get(type)); + } + + /** + * Start building a {@link ListBinder}. + * + * @param binder The current binder, usually {@link AbstractModule#binder()}. + * @param type The type of the list element. + * @param <T> The type of the list element. + * @return A fluent builder. + */ + public static <T> AnnotatedListBinderBuilder<T> build(Binder binder, TypeLiteral<T> type) { + return new BuilderImpl<>(binder.skipSources(SKIPPED_SOURCES), type, PotentialAnnotation.none()); + } + + private static class BuilderImpl<T> implements AnnotatedListBinderBuilder<T> { + private final Binder binder; + private final TypeLiteral<T> entryType; + private final PotentialAnnotation potentialAnnotation; + + BuilderImpl(Binder binder, TypeLiteral<T> type, PotentialAnnotation potentialAnnotation) { + this.binder = binder; + this.entryType = type; + this.potentialAnnotation = potentialAnnotation; + } + + @Override + public ListBinderBuilder<T> annotatedWith(Class<? extends Annotation> annotationType) { + return new BuilderImpl<>(binder, entryType, potentialAnnotation.annotatedWith(annotationType)); + } + + @Override + public ListBinderBuilder<T> annotatedWith(Annotation annotation) { + return new BuilderImpl<>(binder, entryType, potentialAnnotation.annotatedWith(annotation)); + } + + @Override + public ListBinder<T> withDefaultPriority() { + return create(Priority.getDefault()); + } + + @Override + public ListBinder<T> withPriority(int weight, int... weights) { + return create(Priority.create(weight, weights)); + } + + private ListBinder<T> create(Priority priority) { + ListBinder<T> listBinder = new ListBinder<>(binder, entryType, potentialAnnotation, priority); + + // Add the delayed errors + Message duplicateBindersError = new Message(PrettyTypes.format("Duplicate %s", listBinder)); + Message conflictingDefaultExplicitError; + if (priority.isDefault()) { + conflictingDefaultExplicitError = new Message(PrettyTypes.format("%s conflicts with ListBinder with explicit priority", listBinder)); + } else { + conflictingDefaultExplicitError = new Message(PrettyTypes.format("%s conflicts with ListBinder with default priority", listBinder)); + } + listBinder.errorMultibinder.addBinding().toInstance(new ListBinderErrors<T>( + priority, + duplicateBindersError, + conflictingDefaultExplicitError)); + + // Set up the exposed bindings + binder.bind(listBinder.listOfProvidersKey) + .toProvider(new ListOfProvidersProvider<>(listBinder)); + binder.bind(listBinder.listKey) + .toProvider(new ListOfProvidersAdapter<>(listBinder.listOfProvidersKey)); + + return listBinder; + } + } + + /** + * Provider implementation for {@code List<Provider<T>>}. + */ + private static class ListOfProvidersProvider<T> implements Provider<List<Provider<T>>> { + private final Key<Set<ListElement<T>>> setKey; + private final Key<Set<ListBinderErrors<T>>> errorSetKey; + private final Priority priority; + private List<Provider<T>> providers; + + ListOfProvidersProvider(ListBinder<T> listBinder) { + this.setKey = listBinder.setKey; + this.errorSetKey = listBinder.errorSetKey; + this.priority = listBinder.initialPriority; + } + + @Inject + void inject(Injector injector) { + validate(injector); + initialize(injector); + } + + private void validate(Injector injector) { + // Note that here we don't report all errors at once, correctness relies on Guice injecting even providers + // that get de-duplicated. This way, all errors are attached to the right source. + + List<Message> messages = new ArrayList<>(); + + // Get the errors into a multimap by priority + Set<ListBinderErrors<T>> errorSet = injector.getInstance(errorSetKey); + ListMultimap<Priority, ListBinderErrors<T>> errorMap = ArrayListMultimap.create(); + for (ListBinderErrors<T> errors : errorSet) { + errorMap.put(errors.priority, errors); + } + + // Check for duplicate priorities + List<ListBinderErrors<T>> ourPriorityErrors = errorMap.get(priority); + ListBinderErrors<T> ourErrors = ourPriorityErrors.get(0); + if (ourPriorityErrors.size() > 1) { + messages.add(ourErrors.duplicateBindersError); + } + + // Check for default and non-default priorities + if (errorMap.containsKey(Priority.getDefault()) && errorMap.keySet().size() > 1) { + messages.add(ourErrors.conflictingDefaultExplicitError); + } + + if (!messages.isEmpty()) { + throw new CreationException(messages); + } + } + + private void initialize(final Injector injector) { + Set<ListElement<T>> set = injector.getInstance(setKey); + List<ListElement<T>> elements = new ArrayList<>(set); + Collections.sort(elements); + + this.providers = FluentIterable.from(elements) + .transform(new Function<ListElement<T>, Provider<T>>() { + @Override + public Provider<T> apply(ListElement<T> input) { + return injector.getProvider(input.key); + } + }) + .toList(); + } + + @Override + public List<Provider<T>> get() { + return providers; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } else if (!(obj instanceof ListOfProvidersProvider)) { + return false; + } + + ListOfProvidersProvider<?> other = (ListOfProvidersProvider<?>)obj; + return setKey.equals(other.setKey); + } + + @Override + public int hashCode() { + return setKey.hashCode(); + } + } + + /** + * Provider implementation for {@code List<T>}, in terms of {@code List<Provider<T>>}. + */ + private static class ListOfProvidersAdapter<T> implements Provider<List<T>> { + private final Key<List<Provider<T>>> providerListKey; + private Provider<List<Provider<T>>> provider; + + ListOfProvidersAdapter(Key<List<Provider<T>>> providerListKey) { + this.providerListKey = providerListKey; + } + + @Inject + void inject(final Injector injector) { + this.provider = injector.getProvider(providerListKey); + } + + @Override + public List<T> get() { + return FluentIterable.from(provider.get()) + .transform(new Function<Provider<T>, T>() { + @Override + public T apply(Provider<T> input) { + return input.get(); + } + }) + .toList(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } else if (!(obj instanceof ListOfProvidersAdapter)) { + return false; + } + + ListOfProvidersAdapter<?> other = (ListOfProvidersAdapter<?>)obj; + return providerListKey.equals(other.providerListKey); + } + + @Override + public int hashCode() { + return Objects.hashCode(providerListKey); + } + } + + /** + * Add an entry to the list. + * + * <p> + * The entry will be added in order for this {@link ListBinder} instance. Between different {@link ListBinder}s, the + * order is determined by the {@link ListBinder}'s {@link Priority}. + * </p> + * + * @return A fluent binding builder. + */ + public LinkedBindingBuilder<T> addBinding() { + Key<T> key = Key.get(entryType, UniqueAnnotations.create()); + multibinder.addBinding().toInstance(new ListElement<>(key, priority)); + priority = priority.next(); + return binder.bind(key); + } + + @Override + public String toString() { + return PrettyTypes.format("ListBinder<%s>%s with %s", + entryType, + (potentialAnnotation.hasAnnotation() ? " annotated with " + potentialAnnotation : ""), + initialPriority); + } +} diff --git a/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinderBuilder.java b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinderBuilder.java new file mode 100644 index 0000000..5dae594 --- /dev/null +++ b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinderBuilder.java @@ -0,0 +1,24 @@ +package com.tavianator.sangria.listbinder; + +import com.tavianator.sangria.core.Priority; + +/** + * Fluent builder interface. + * + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.1 + * @since 1.1 + */ +public interface ListBinderBuilder<T> { + /** + * @return A {@link ListBinder} with the default priority. + * @see Priority + */ + ListBinder<T> withDefaultPriority(); + + /** + * @return A {@link ListBinder} with the given priority. + * @see Priority + */ + ListBinder<T> withPriority(int weight, int... weights); +} diff --git a/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinderErrors.java b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinderErrors.java new file mode 100644 index 0000000..dfd0c64 --- /dev/null +++ b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinderErrors.java @@ -0,0 +1,43 @@ +/**************************************************************************** + * Sangria * + * Copyright (C) 2014 Tavian Barnes <tavianator@tavianator.com> * + * * + * 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. * + ****************************************************************************/ + +package com.tavianator.sangria.listbinder; + +import com.google.inject.Key; +import com.google.inject.spi.Message; + +import com.tavianator.sangria.core.Priority; + +/** + * Error holder for {@link ListBinder}s. + * + * @param <T> Only used to allow different {@link Key}s. + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.1 + * @since 1.1 + */ +class ListBinderErrors<T> { + final Priority priority; + final Message duplicateBindersError; + final Message conflictingDefaultExplicitError; + + ListBinderErrors(Priority priority, Message duplicateBindersError, Message conflictingDefaultExplicitError) { + this.priority = priority; + this.duplicateBindersError = duplicateBindersError; + this.conflictingDefaultExplicitError = conflictingDefaultExplicitError; + } +} diff --git a/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListElement.java b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListElement.java new file mode 100644 index 0000000..bb95674 --- /dev/null +++ b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListElement.java @@ -0,0 +1,44 @@ +/**************************************************************************** + * Sangria * + * Copyright (C) 2014 Tavian Barnes <tavianator@tavianator.com> * + * * + * 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. * + ****************************************************************************/ + +package com.tavianator.sangria.listbinder; + +import com.google.inject.Key; + +import com.tavianator.sangria.core.Priority; + +/** + * An individual element in a ListBinder. + * + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.1 + * @since 1.1 + */ +class ListElement<T> implements Comparable<ListElement<T>> { + final Key<T> key; + final Priority priority; + + ListElement(Key<T> key, Priority priority) { + this.key = key; + this.priority = priority; + } + + @Override + public int compareTo(ListElement<T> o) { + return priority.compareTo(o.priority); + } +} diff --git a/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/package-info.java b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/package-info.java new file mode 100644 index 0000000..1ed1af4 --- /dev/null +++ b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/package-info.java @@ -0,0 +1,25 @@ +/**************************************************************************** + * Sangria * + * Copyright (C) 2014 Tavian Barnes <tavianator@tavianator.com> * + * * + * 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. * + ****************************************************************************/ + +/** + * {@code sangria-listbinder}: A multi-binder with guaranteed order. + * + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.1 + * @since 1.1 + */ +package com.tavianator.sangria.listbinder; diff --git a/sangria-listbinder/src/test/java/com/tavianator/sangria/listbinder/ListBinderTest.java b/sangria-listbinder/src/test/java/com/tavianator/sangria/listbinder/ListBinderTest.java new file mode 100644 index 0000000..4c7c86b --- /dev/null +++ b/sangria-listbinder/src/test/java/com/tavianator/sangria/listbinder/ListBinderTest.java @@ -0,0 +1,210 @@ +/**************************************************************************** + * Sangria * + * Copyright (C) 2014 Tavian Barnes <tavianator@tavianator.com> * + * * + * 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. * + ****************************************************************************/ + +package com.tavianator.sangria.listbinder; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.*; +import javax.inject.Provider; +import javax.inject.Qualifier; + +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.TypeLiteral; +import com.google.inject.name.Names; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import com.tavianator.sangria.core.TypeLiterals; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +/** + * Tests for {@link ListBinder}. + * + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.1 + * @since 1.1 + */ +public class ListBinderTest { + public @Rule ExpectedException thrown = ExpectedException.none(); + + @Retention(RetentionPolicy.RUNTIME) + @Qualifier + private @interface Simple { + } + + private static final TypeLiteral<List<String>> LIST_OF_STRINGS = TypeLiterals.listOf(String.class); + private static final TypeLiteral<List<Provider<String>>> LIST_OF_STRING_PROVIDERS = TypeLiterals.listOf(TypeLiterals.providerOf(String.class)); + + @Test + public void testBasicLists() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + ListBinder<String> listBinder = ListBinder.build(binder(), String.class) + .withDefaultPriority(); + listBinder.addBinding().toInstance("a"); + listBinder.addBinding().toInstance("b"); + listBinder.addBinding().toInstance("c"); + + listBinder = ListBinder.build(binder(), String.class) + .annotatedWith(Simple.class) + .withDefaultPriority(); + listBinder.addBinding().toInstance("d"); + listBinder.addBinding().toInstance("e"); + listBinder.addBinding().toInstance("f"); + + listBinder = ListBinder.build(binder(), String.class) + .annotatedWith(Names.named("name")) + .withDefaultPriority(); + listBinder.addBinding().toInstance("g"); + listBinder.addBinding().toInstance("h"); + listBinder.addBinding().toInstance("i"); + } + }); + List<String> list = injector.getInstance(Key.get(LIST_OF_STRINGS)); + assertThat(list, contains("a", "b", "c")); + + List<Provider<String>> providers = injector.getInstance(Key.get(LIST_OF_STRING_PROVIDERS)); + assertThat(providers, hasSize(3)); + assertThat(providers.get(0).get(), equalTo("a")); + assertThat(providers.get(1).get(), equalTo("b")); + assertThat(providers.get(2).get(), equalTo("c")); + + list = injector.getInstance(Key.get(LIST_OF_STRINGS, Simple.class)); + assertThat(list, contains("d", "e", "f")); + + list = injector.getInstance(Key.get(LIST_OF_STRINGS, Names.named("name"))); + assertThat(list, contains("g", "h", "i")); + } + + @Test + public void testSplitBinders() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + ListBinder<String> listBinder = ListBinder.build(binder(), String.class) + .withPriority(1); + listBinder.addBinding().toInstance("c"); + listBinder.addBinding().toInstance("d"); + } + }, new AbstractModule() { + @Override + protected void configure() { + ListBinder<String> listBinder = ListBinder.build(binder(), String.class) + .withPriority(0); + listBinder.addBinding().toInstance("a"); + listBinder.addBinding().toInstance("b"); + } + }); + List<String> list = injector.getInstance(Key.get(LIST_OF_STRINGS)); + assertThat(list, contains("a", "b", "c", "d")); + } + + @Test + public void testConflictingDefaultPriorities() { + thrown.expect(CreationException.class); + thrown.expectMessage(containsString("2 errors")); + thrown.expectMessage(containsString("1) Duplicate ListBinder<java.lang.String> with default priority")); + thrown.expectMessage(containsString("2) Duplicate ListBinder<java.lang.String> with default priority")); + thrown.expectMessage(containsString("at com.tavianator.sangria.listbinder.ListBinderTest")); + + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + ListBinder.build(binder(), String.class) + .withDefaultPriority(); + } + }, new AbstractModule() { + @Override + protected void configure() { + ListBinder.build(binder(), String.class) + .withDefaultPriority(); + } + }); + } + + @Test + public void testConflictingExplicitPriorities() { + thrown.expect(CreationException.class); + thrown.expectMessage(containsString("2 errors")); + thrown.expectMessage(containsString("1) Duplicate ListBinder<java.lang.String> with priority [1]")); + thrown.expectMessage(containsString("2) Duplicate ListBinder<java.lang.String> with priority [1]")); + thrown.expectMessage(containsString("at com.tavianator.sangria.listbinder.ListBinderTest")); + + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + ListBinder.build(binder(), String.class) + .withPriority(1); + } + }, new AbstractModule() { + @Override + protected void configure() { + ListBinder.build(binder(), String.class) + .withPriority(1); + } + }); + } + + @Test + public void testConflictingDefaultAndExplicitPriorities() { + thrown.expect(CreationException.class); + thrown.expectMessage(containsString("2 errors")); + thrown.expectMessage(containsString(") ListBinder<java.lang.String> with default priority conflicts with ListBinder with explicit priority")); + thrown.expectMessage(containsString(") ListBinder<java.lang.String> with priority [1] conflicts with ListBinder with default priority")); + thrown.expectMessage(containsString("at com.tavianator.sangria.listbinder.ListBinderTest")); + + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + ListBinder.build(binder(), String.class) + .withDefaultPriority(); + } + }, new AbstractModule() { + @Override + protected void configure() { + ListBinder.build(binder(), String.class) + .withPriority(1); + } + }); + } + + @Test + public void testToString() { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + ListBinder<String> listBinder = ListBinder.build(binder(), String.class) + .annotatedWith(Names.named("name")) + .withDefaultPriority(); + assertThat(listBinder.toString(), equalTo("ListBinder<java.lang.String> annotated with @com.google.inject.name.Named(value=name) with default priority")); + + ListBinder<Object> objectListBinder = ListBinder.build(binder(), Object.class) + .withPriority(1, 2); + assertThat(objectListBinder.toString(), equalTo("ListBinder<java.lang.Object> with priority [1, 2]")); + } + }); + } +} diff --git a/sangria-log4j/pom.xml b/sangria-log4j/pom.xml index 085a007..5dae892 100644 --- a/sangria-log4j/pom.xml +++ b/sangria-log4j/pom.xml @@ -7,7 +7,7 @@ <parent> <groupId>com.tavianator.sangria</groupId> <artifactId>sangria</artifactId> - <version>1.0</version> + <version>1.1-SNAPSHOT</version> </parent> <artifactId>sangria-log4j</artifactId> diff --git a/sangria-slf4j/pom.xml b/sangria-slf4j/pom.xml index 46cec57..fc69f41 100644 --- a/sangria-slf4j/pom.xml +++ b/sangria-slf4j/pom.xml @@ -7,7 +7,7 @@ <parent> <groupId>com.tavianator.sangria</groupId> <artifactId>sangria</artifactId> - <version>1.0</version> + <version>1.1-SNAPSHOT</version> </parent> <artifactId>sangria-slf4j</artifactId> |