com.tavianator.sangria.listbinder.ListBinder Maven / Gradle / Ivy
Show all versions of sangria-listbinder Show documentation
/****************************************************************************
* Sangria *
* Copyright (C) 2014 Tavian Barnes *
* *
* 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.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.
*
*
* {@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:
*
*
*
* ListBinder<String> listBinder = ListBinder.build(binder(), String.class)
* .withDefaultPriority();
* listBinder.addBinding().toInstance("a");
* listBinder.addBinding().toInstance("b");
*
*
*
* This will create a binding for a {@code List}, which contains {@code "a"} followed by {@code "b"}. It also
* creates a binding for {@code List>} — this may be useful in more advanced cases to allow list
* elements to be lazily loaded.
*
*
* To add an annotation to the list binding, simply write this:
*
*
* ListBinder<String> listBinder = ListBinder.build(binder(), String.class)
* .annotatedWith(Names.named("name"))
* .withDefaultPriority();
*
*
*
* and the created binding will be {@code @Named("name") List} instead.
*
*
*
* For large lists, it may be helpful to split up their specification across different modules. This is accomplished by
* specifying priorities for the {@link ListBinder}s when they are created. For example:
*
*
*
* // 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");
*
*
*
* 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}.
*
*
* @param The type of the list element.
* @author Tavian Barnes ([email protected])
* @version 1.1
* @since 1.1
*/
public class ListBinder {
private static final Class>[] SKIPPED_SOURCES = {
ListBinder.class,
BuilderImpl.class,
};
private final Binder binder;
private final Multibinder> multibinder;
private final Multibinder> errorMultibinder;
private final TypeLiteral entryType;
private final Key> listKey;
private final Key>> listOfProvidersKey;
private final Key>> setKey;
private final Key>> errorSetKey;
private final PotentialAnnotation potentialAnnotation;
private final Priority initialPriority;
private Priority priority;
private ListBinder(
Binder binder,
TypeLiteral entryType,
PotentialAnnotation potentialAnnotation,
Priority initialPriority) {
this.binder = binder;
this.entryType = entryType;
TypeLiteral> elementType = listElementOf(entryType);
TypeLiteral> 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 TypeLiteral> listElementOf(TypeLiteral type) {
return (TypeLiteral>)TypeLiteral.get(Types.newParameterizedType(ListElement.class, type.getType()));
}
@SuppressWarnings("unchecked")
private static TypeLiteral> listBinderErrorsOf(TypeLiteral type) {
return (TypeLiteral>)TypeLiteral.get(Types.newParameterizedType(ListBinderErrors.class, type.getType()));
}
/**
* {@link PotentialAnnotation.Visitor} that makes {@link Multibinder}s with the given annotation.
*/
private static class MultibinderMaker implements PotentialAnnotation.Visitor> {
private final Binder binder;
private final TypeLiteral type;
MultibinderMaker(Binder binder, TypeLiteral type) {
this.binder = binder;
this.type = type;
}
@Override
public Multibinder visitNoAnnotation() {
return Multibinder.newSetBinder(binder, type);
}
@Override
public Multibinder visitAnnotationType(Class extends Annotation> annotationType) {
return Multibinder.newSetBinder(binder, type, annotationType);
}
@Override
public Multibinder 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 The type of the list element.
* @return A fluent builder.
*/
public static AnnotatedListBinderBuilder build(Binder binder, Class 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 The type of the list element.
* @return A fluent builder.
*/
public static AnnotatedListBinderBuilder build(Binder binder, TypeLiteral type) {
return new BuilderImpl<>(binder.skipSources(SKIPPED_SOURCES), type, PotentialAnnotation.none());
}
private static class BuilderImpl implements AnnotatedListBinderBuilder {
private final Binder binder;
private final TypeLiteral entryType;
private final PotentialAnnotation potentialAnnotation;
BuilderImpl(Binder binder, TypeLiteral type, PotentialAnnotation potentialAnnotation) {
this.binder = binder;
this.entryType = type;
this.potentialAnnotation = potentialAnnotation;
}
@Override
public ListBinderBuilder annotatedWith(Class extends Annotation> annotationType) {
return new BuilderImpl<>(binder, entryType, potentialAnnotation.annotatedWith(annotationType));
}
@Override
public ListBinderBuilder annotatedWith(Annotation annotation) {
return new BuilderImpl<>(binder, entryType, potentialAnnotation.annotatedWith(annotation));
}
@Override
public ListBinder withDefaultPriority() {
return create(Priority.getDefault());
}
@Override
public ListBinder withPriority(int weight, int... weights) {
return create(Priority.create(weight, weights));
}
private ListBinder create(Priority priority) {
ListBinder 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(
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 implements Provider>> {
private final Key>> setKey;
private final Key>> errorSetKey;
private final Priority priority;
private List> providers;
ListOfProvidersProvider(ListBinder 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 messages = new ArrayList<>();
// Get the errors into a multimap by priority
Set> errorSet = injector.getInstance(errorSetKey);
ListMultimap> errorMap = ArrayListMultimap.create();
for (ListBinderErrors errors : errorSet) {
errorMap.put(errors.priority, errors);
}
// Check for duplicate priorities
List> ourPriorityErrors = errorMap.get(priority);
ListBinderErrors 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> set = injector.getInstance(setKey);
List> elements = new ArrayList<>(set);
Collections.sort(elements);
this.providers = FluentIterable.from(elements)
.transform(new Function, Provider>() {
@Override
public Provider apply(ListElement input) {
return injector.getProvider(input.key);
}
})
.toList();
}
@Override
public List> 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 implements Provider> {
private final Key>> providerListKey;
private Provider>> provider;
ListOfProvidersAdapter(Key>> providerListKey) {
this.providerListKey = providerListKey;
}
@Inject
void inject(final Injector injector) {
this.provider = injector.getProvider(providerListKey);
}
@Override
public List get() {
return FluentIterable.from(provider.get())
.transform(new Function, T>() {
@Override
public T apply(Provider 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 providerListKey.hashCode();
}
}
/**
* Add an entry to the list.
*
*
* 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}.
*
*
* @return A fluent binding builder.
*/
public LinkedBindingBuilder addBinding() {
Key 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);
}
}