/** | |
* Copyright (C) 2010 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. | |
*/ | |
package com.google.inject.multibindings; | |
import static com.google.inject.multibindings.MapBinder.entryOfProviderOf; | |
import static com.google.inject.multibindings.MapBinder.mapOf; | |
import static com.google.inject.multibindings.MapBinder.mapOfProviderOf; | |
import static com.google.inject.multibindings.MapBinder.mapOfSetOfProviderOf; | |
import static com.google.inject.multibindings.Multibinder.setOf; | |
import static com.google.inject.multibindings.SpiUtils.BindType.INSTANCE; | |
import static com.google.inject.multibindings.SpiUtils.BindType.LINKED; | |
import static com.google.inject.multibindings.SpiUtils.BindType.PROVIDER_INSTANCE; | |
import static com.google.inject.multibindings.SpiUtils.VisitType.BOTH; | |
import static com.google.inject.multibindings.SpiUtils.VisitType.INJECTOR; | |
import static com.google.inject.multibindings.SpiUtils.VisitType.MODULE; | |
import static junit.framework.Assert.assertEquals; | |
import static junit.framework.Assert.assertNotNull; | |
import static junit.framework.Assert.assertTrue; | |
import static junit.framework.Assert.fail; | |
import java.util.HashSet; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Set; | |
import com.google.inject.Binding; | |
import com.google.inject.Guice; | |
import com.google.inject.Injector; | |
import com.google.inject.Key; | |
import com.google.inject.Module; | |
import com.google.inject.TypeLiteral; | |
import com.google.inject.internal.util.Lists; | |
import com.google.inject.spi.DefaultBindingTargetVisitor; | |
import com.google.inject.spi.Element; | |
import com.google.inject.spi.Elements; | |
import com.google.inject.spi.InstanceBinding; | |
import com.google.inject.spi.LinkedKeyBinding; | |
import com.google.inject.spi.ProviderInstanceBinding; | |
import com.google.inject.spi.ProviderLookup; | |
/** | |
* Utilities for testing the Multibinder & MapBinder extension SPI. | |
* | |
* @author sameb@google.com (Sam Berlin) | |
*/ | |
public class SpiUtils { | |
/** The kind of test we should perform. A live Injector, a raw Elements (Module) test, or both. */ | |
enum VisitType { INJECTOR, MODULE, BOTH } | |
/** | |
* Asserts that MapBinderBinding visitors for work correctly. | |
* | |
* @param <T> The type of the binding | |
* @param mapKey The key the map belongs to. | |
* @param keyType the TypeLiteral of the key of the map | |
* @param valueType the TypeLiteral of the value of the map | |
* @param modules The modules that define the mapbindings | |
* @param visitType The kind of test we should perform. A live Injector, a raw Elements (Module) test, or both. | |
* @param allowDuplicates If duplicates are allowed. | |
* @param expectedMapBindings The number of other mapbinders we expect to see. | |
* @param results The kind of bindings contained in the mapbinder. | |
*/ | |
static <T> void assertMapVisitor(Key<T> mapKey, TypeLiteral<?> keyType, TypeLiteral<?> valueType, | |
Iterable<? extends Module> modules, VisitType visitType, boolean allowDuplicates, | |
int expectedMapBindings, MapResult... results) { | |
if(visitType == null) { | |
fail("must test something"); | |
} | |
if (visitType == BOTH || visitType == INJECTOR) { | |
mapInjectorTest(mapKey, keyType, valueType, modules, allowDuplicates, expectedMapBindings, | |
results); | |
} | |
if (visitType == BOTH || visitType == MODULE) { | |
mapModuleTest(mapKey, keyType, valueType, modules, allowDuplicates, expectedMapBindings, | |
results); | |
} | |
} | |
@SuppressWarnings("unchecked") | |
private static <T> void mapInjectorTest(Key<T> mapKey, TypeLiteral<?> keyType, | |
TypeLiteral<?> valueType, Iterable<? extends Module> modules, boolean allowDuplicates, | |
int expectedMapBindings, MapResult... results) { | |
Injector injector = Guice.createInjector(modules); | |
Visitor<T> visitor = new Visitor<T>(); | |
Binding<T> mapBinding = injector.getBinding(mapKey); | |
MapBinderBinding<T> mapbinder = (MapBinderBinding<T>)mapBinding.acceptTargetVisitor(visitor); | |
assertNotNull(mapbinder); | |
assertEquals(keyType, mapbinder.getKeyTypeLiteral()); | |
assertEquals(valueType, mapbinder.getValueTypeLiteral()); | |
assertEquals(allowDuplicates, mapbinder.permitsDuplicates()); | |
List<Map.Entry<?, Binding<?>>> entries = Lists.newArrayList(mapbinder.getEntries()); | |
List<MapResult> mapResults = Lists.newArrayList(results); | |
assertEquals("wrong entries, expected: " + mapResults + ", but was: " + entries, | |
mapResults.size(), entries.size()); | |
for(MapResult result : mapResults) { | |
Map.Entry<?, Binding<?>> found = null; | |
for(Map.Entry<?, Binding<?>> entry : entries) { | |
Object key = entry.getKey(); | |
Binding<?> value = entry.getValue(); | |
if(!key.equals(result.k)) { | |
continue; | |
} | |
switch (result.v.type) { | |
case INSTANCE: | |
if (value instanceof InstanceBinding | |
&& ((InstanceBinding) value).getInstance().equals(result.v.instance)) { | |
found = entry; | |
} | |
break; | |
case LINKED: | |
if (value instanceof LinkedKeyBinding | |
&& ((LinkedKeyBinding) value).getKey().equals(result.v.key)) { | |
found = entry; | |
} | |
break; | |
case PROVIDER_INSTANCE: | |
if (value instanceof ProviderInstanceBinding | |
&& ((ProviderInstanceBinding) value).getProviderInstance().get().equals( | |
result.v.instance)) { | |
found = entry; | |
} | |
break; | |
} | |
} | |
if(found == null) { | |
fail("Could not find entry: " + result + " in remaining entries: " + entries); | |
} else { | |
assertTrue(mapbinder.containsElement(found.getValue())); | |
entries.remove(found); | |
} | |
} | |
if(!entries.isEmpty()) { | |
fail("Found all entries of: " + mapResults + ", but more were left over: " + entries); | |
} | |
Key<?> mapOfProvider = adapt(mapKey, mapOfProviderOf(keyType, valueType)); | |
Key<?> mapOfSetOfProvider = adapt(mapKey, mapOfSetOfProviderOf(keyType, valueType)); | |
Key<?> mapOfSet = adapt(mapKey, mapOf(keyType, setOf(valueType))); | |
Key<?> setOfEntry = adapt(mapKey, setOf(entryOfProviderOf(keyType, valueType))); | |
boolean entrySetMatch = false; | |
boolean mapProviderMatch = false; | |
boolean mapSetMatch = false; | |
boolean mapSetProviderMatch = false; | |
List<Object> otherMapBindings = Lists.newArrayList(); | |
List<Binding> otherMatches = Lists.newArrayList(); | |
for(Binding b : injector.getAllBindings().values()) { | |
boolean contains = mapbinder.containsElement(b); | |
Object visited = b.acceptTargetVisitor(visitor); | |
if(visited instanceof MapBinderBinding) { | |
if(visited.equals(mapbinder)) { | |
assertTrue(contains); | |
} else { | |
otherMapBindings.add(visited); | |
} | |
} else if(b.getKey().equals(mapOfProvider)) { | |
assertTrue(contains); | |
mapProviderMatch = true; | |
} else if(b.getKey().equals(mapOfSet)) { | |
assertTrue(contains); | |
mapSetMatch = true; | |
} else if(b.getKey().equals(mapOfSetOfProvider)) { | |
assertTrue(contains); | |
mapSetProviderMatch = true; | |
} else if(b.getKey().equals(setOfEntry)) { | |
assertTrue(contains); | |
entrySetMatch = true; | |
// Validate that this binding is also a MultibinderBinding. | |
assertTrue(b.acceptTargetVisitor(visitor) instanceof MultibinderBinding); | |
} else if (contains) { | |
otherMatches.add(b); | |
} | |
} | |
int sizeOfOther = otherMatches.size(); | |
if(allowDuplicates) { | |
sizeOfOther--; // account for 1 duplicate binding | |
} | |
sizeOfOther = sizeOfOther / 2; // account for 1 value & 1 Map.Entry of each expected binding. | |
assertEquals("Incorrect other matches: " + otherMatches, mapResults.size(), sizeOfOther); | |
assertTrue(entrySetMatch); | |
assertTrue(mapProviderMatch); | |
assertEquals(allowDuplicates, mapSetMatch); | |
assertEquals(allowDuplicates, mapSetProviderMatch); | |
assertEquals("other MapBindings found: " + otherMapBindings, expectedMapBindings, | |
otherMapBindings.size()); | |
} | |
/** Adapts a key, keeping the original annotation, using the new type literal. */ | |
private static Key<?> adapt(Key<?> mapKey, TypeLiteral<?> resultType) { | |
if(mapKey.getAnnotation() != null) { | |
return Key.get(resultType, mapKey.getAnnotation()); | |
} else if(mapKey.getAnnotationType() != null) { | |
return Key.get(resultType, mapKey.getAnnotationType()); | |
} else { | |
return Key.get(resultType); | |
} | |
} | |
@SuppressWarnings("unchecked") | |
private static <T> void mapModuleTest(Key<T> mapKey, TypeLiteral<?> keyType, | |
TypeLiteral<?> valueType, Iterable<? extends Module> modules, boolean allowDuplicates, | |
int expectedMapBindings, MapResult... results) { | |
List<Element> elements = Elements.getElements(modules); | |
Visitor<T> visitor = new Visitor<T>(); | |
MapBinderBinding<T> mapbinder = null; | |
for(Element element : elements) { | |
if(element instanceof Binding && ((Binding)element).getKey().equals(mapKey)) { | |
mapbinder = (MapBinderBinding<T>)((Binding)element).acceptTargetVisitor(visitor); | |
break; | |
} | |
} | |
assertNotNull(mapbinder); | |
assertEquals(keyType, mapbinder.getKeyTypeLiteral()); | |
assertEquals(valueType, mapbinder.getValueTypeLiteral()); | |
List<MapResult> mapResults = Lists.newArrayList(results); | |
Key<?> mapOfProvider = adapt(mapKey, mapOfProviderOf(keyType, valueType)); | |
Key<?> mapOfSetOfProvider = adapt(mapKey, mapOfSetOfProviderOf(keyType, valueType)); | |
Key<?> mapOfSet = adapt(mapKey, mapOf(keyType, setOf(valueType))); | |
Key<?> setOfEntry = adapt(mapKey, setOf(entryOfProviderOf(keyType, valueType))); | |
boolean entrySetMatch = false; | |
boolean mapProviderMatch = false; | |
boolean mapSetMatch = false; | |
boolean mapSetProviderMatch = false; | |
List<Object> otherMapBindings = Lists.newArrayList(); | |
List<Element> otherMatches = Lists.newArrayList(); | |
List<Element> otherElements = Lists.newArrayList(); | |
for(Element element : elements) { | |
boolean contains = mapbinder.containsElement(element); | |
if(!contains) { | |
otherElements.add(element); | |
} | |
boolean matched = false; | |
Key key = null; | |
Binding b = null; | |
if(element instanceof Binding) { | |
b = (Binding)element; | |
key = b.getKey(); | |
Object visited = b.acceptTargetVisitor(visitor); | |
if(visited instanceof MapBinderBinding) { | |
matched = true; | |
if(visited.equals(mapbinder)) { | |
assertTrue(contains); | |
} else { | |
otherMapBindings.add(visited); | |
} | |
} | |
} else if(element instanceof ProviderLookup) { | |
key = ((ProviderLookup)element).getKey(); | |
} | |
if(!matched && key != null) { | |
if(key.equals(mapOfProvider)) { | |
matched = true; | |
assertTrue(contains); | |
mapProviderMatch = true; | |
} else if(key.equals(mapOfSet)) { | |
matched = true; | |
assertTrue(contains); | |
mapSetMatch = true; | |
} else if(key.equals(mapOfSetOfProvider)) { | |
matched = true; | |
assertTrue(contains); | |
mapSetProviderMatch = true; | |
} else if(key.equals(setOfEntry)) { | |
matched = true; | |
assertTrue(contains); | |
entrySetMatch = true; | |
// Validate that this binding is also a MultibinderBinding. | |
if(b != null) { | |
assertTrue(b.acceptTargetVisitor(visitor) instanceof MultibinderBinding); | |
} | |
} | |
} | |
if(!matched && contains) { | |
otherMatches.add(element); | |
} | |
} | |
int otherMatchesSize = otherMatches.size(); | |
if(allowDuplicates) { | |
otherMatchesSize--; // allow for 1 duplicate binding | |
} | |
otherMatchesSize = otherMatchesSize / 3; // value, ProviderLookup per value, Map.Entry per value | |
assertEquals("incorrect number of contains, leftover matches: " + otherMatches, mapResults | |
.size(), otherMatchesSize); | |
assertTrue(entrySetMatch); | |
assertTrue(mapProviderMatch); | |
assertEquals(allowDuplicates, mapSetMatch); | |
assertEquals(allowDuplicates, mapSetProviderMatch); | |
assertEquals("other MapBindings found: " + otherMapBindings, expectedMapBindings, | |
otherMapBindings.size()); | |
// Validate that we can construct an injector out of the remaining bindings. | |
Guice.createInjector(Elements.getModule(otherElements)); | |
} | |
/** | |
* Asserts that MultibinderBinding visitors work correctly. | |
* | |
* @param <T> The type of the binding | |
* @param setKey The key the set belongs to. | |
* @param elementType the TypeLiteral of the element | |
* @param modules The modules that define the multibindings | |
* @param visitType The kind of test we should perform. A live Injector, a raw Elements (Module) test, or both. | |
* @param allowDuplicates If duplicates are allowed. | |
* @param expectedMultibindings The number of other multibinders we expect to see. | |
* @param results The kind of bindings contained in the multibinder. | |
*/ | |
static <T> void assertSetVisitor(Key<T> setKey, TypeLiteral<?> elementType, | |
Iterable<? extends Module> modules, VisitType visitType, boolean allowDuplicates, | |
int expectedMultibindings, BindResult... results) { | |
if(visitType == null) { | |
fail("must test something"); | |
} | |
if(visitType == BOTH || visitType == INJECTOR) { | |
setInjectorTest(setKey, elementType, modules, allowDuplicates, expectedMultibindings, results); | |
} | |
if(visitType == BOTH || visitType == MODULE) { | |
setModuleTest(setKey, elementType, modules, allowDuplicates, expectedMultibindings, results); | |
} | |
} | |
@SuppressWarnings("unchecked") | |
private static <T> void setInjectorTest(Key<T> setKey, TypeLiteral<?> elementType, | |
Iterable<? extends Module> modules, boolean allowDuplicates, int otherMultibindings, | |
BindResult... results) { | |
Injector injector = Guice.createInjector(modules); | |
Visitor<T> visitor = new Visitor<T>(); | |
Binding<T> binding = injector.getBinding(setKey); | |
MultibinderBinding<T> multibinder = (MultibinderBinding<T>)binding.acceptTargetVisitor(visitor); | |
assertNotNull(multibinder); | |
assertEquals(elementType, multibinder.getElementTypeLiteral()); | |
assertEquals(allowDuplicates, multibinder.permitsDuplicates()); | |
List<Binding<?>> elements = Lists.newArrayList(multibinder.getElements()); | |
List<BindResult> bindResults = Lists.newArrayList(results); | |
assertEquals("wrong bind elements, expected: " + bindResults + ", but was: " + multibinder.getElements(), | |
bindResults.size(), elements.size()); | |
for(BindResult result : bindResults) { | |
Binding found = null; | |
for(Binding item : elements) { | |
switch (result.type) { | |
case INSTANCE: | |
if (item instanceof InstanceBinding | |
&& ((InstanceBinding) item).getInstance().equals(result.instance)) { | |
found = item; | |
} | |
break; | |
case LINKED: | |
if (item instanceof LinkedKeyBinding | |
&& ((LinkedKeyBinding) item).getKey().equals(result.key)) { | |
found = item; | |
} | |
break; | |
case PROVIDER_INSTANCE: | |
if (item instanceof ProviderInstanceBinding | |
&& ((ProviderInstanceBinding) item).getProviderInstance().get().equals( | |
result.instance)) { | |
found = item; | |
} | |
break; | |
} | |
} | |
if(found == null) { | |
fail("Could not find element: " + result + " in remaining elements: " + elements); | |
} else { | |
elements.remove(found); | |
} | |
} | |
if(!elements.isEmpty()) { | |
fail("Found all elements of: " + bindResults + ", but more were left over: " + elements); | |
} | |
Set<Binding> setOfElements = new HashSet<Binding>(multibinder.getElements()); | |
List<Object> otherMultibinders = Lists.newArrayList(); | |
List<Binding> otherContains = Lists.newArrayList(); | |
for(Binding b : injector.getAllBindings().values()) { | |
boolean contains = multibinder.containsElement(b); | |
Object visited = b.acceptTargetVisitor(visitor); | |
if(visited != null) { | |
if(visited.equals(multibinder)) { | |
assertTrue(contains); | |
} else { | |
otherMultibinders.add(visited); | |
} | |
} else if(setOfElements.contains(b)) { | |
assertTrue(contains); | |
} else if(contains) { | |
otherContains.add(b); | |
} | |
} | |
if(allowDuplicates) { | |
assertEquals("contained more than it should: " + otherContains, 1, otherContains.size()); | |
} else { | |
assertTrue("contained more than it should: " + otherContains, otherContains.isEmpty()); | |
} | |
assertEquals("other multibindings found: " + otherMultibinders, otherMultibindings, | |
otherMultibinders.size()); | |
} | |
@SuppressWarnings("unchecked") | |
private static <T> void setModuleTest(Key<T> setKey, TypeLiteral<?> elementType, | |
Iterable<? extends Module> modules, boolean allowDuplicates, int otherMultibindings, | |
BindResult... results) { | |
List<BindResult> bindResults = Lists.newArrayList(results); | |
List<Element> elements = Elements.getElements(modules); | |
Visitor<T> visitor = new Visitor<T>(); | |
MultibinderBinding<T> multibinder = null; | |
for(Element element : elements) { | |
if(element instanceof Binding && ((Binding)element).getKey().equals(setKey)) { | |
multibinder = (MultibinderBinding<T>)((Binding)element).acceptTargetVisitor(visitor); | |
break; | |
} | |
} | |
assertNotNull(multibinder); | |
assertEquals(elementType, multibinder.getElementTypeLiteral()); | |
List<Object> otherMultibinders = Lists.newArrayList(); | |
Set<Element> otherContains = new HashSet<Element>(); | |
List<Element> otherElements = Lists.newArrayList(); | |
for(Element element : elements) { | |
boolean contains = multibinder.containsElement(element); | |
if(!contains) { | |
otherElements.add(element); | |
} | |
boolean matched = false; | |
if(element instanceof Binding) { | |
Binding binding = (Binding)element; | |
Object visited = binding.acceptTargetVisitor(visitor); | |
if(visited != null) { | |
matched = true; | |
if(visited.equals(multibinder)) { | |
assertTrue(contains); | |
} else { | |
otherMultibinders.add(visited); | |
} | |
} | |
} | |
if(!matched && contains) { | |
otherContains.add(element); | |
} | |
} | |
if(allowDuplicates) { | |
assertEquals("wrong contained elements: " + otherContains, bindResults.size() + 1, otherContains.size()); | |
} else { | |
assertEquals("wrong contained elements: " + otherContains, bindResults.size(), otherContains.size()); | |
} | |
assertEquals("other multibindings found: " + otherMultibinders, otherMultibindings, | |
otherMultibinders.size()); | |
// Validate that we can construct an injector out of the remaining bindings. | |
Guice.createInjector(Elements.getModule(otherElements)); | |
} | |
static <K, V> MapResult instance(K k, V v) { | |
return new MapResult<K, V>(k, new BindResult<V>(INSTANCE, v, null)); | |
} | |
static <K, V> MapResult linked(K k, Class<? extends V> clazz) { | |
return new MapResult<K, V>(k, new BindResult<V>(LINKED, null, Key.get(clazz))); | |
} | |
static <K, V> MapResult linked(K k, Key<? extends V> key) { | |
return new MapResult<K, V>(k, new BindResult<V>(LINKED, null, key)); | |
} | |
static <K, V> MapResult providerInstance(K k, V v) { | |
return new MapResult<K, V>(k, new BindResult<V>(PROVIDER_INSTANCE, v, null)); | |
} | |
private static class MapResult<K, V> { | |
private final K k; | |
private final BindResult<V> v; | |
MapResult(K k, BindResult<V> v) { | |
this.k = k; | |
this.v = v; | |
} | |
@Override | |
public String toString() { | |
return "entry[key[" + k + "],value[" + v + "]]"; | |
} | |
} | |
static <T> BindResult instance(T t) { | |
return new BindResult<T>(INSTANCE, t, null); | |
} | |
static <T> BindResult linked(Class<? extends T> clazz) { | |
return new BindResult<T>(LINKED, null, Key.get(clazz)); | |
} | |
static <T> BindResult linked(Key<? extends T> key) { | |
return new BindResult<T>(LINKED, null, key); | |
} | |
static <T> BindResult providerInstance(T t) { | |
return new BindResult<T>(PROVIDER_INSTANCE, t, null); | |
} | |
/** The kind of binding. */ | |
static enum BindType { INSTANCE, LINKED, PROVIDER_INSTANCE } | |
/** The result of the binding. */ | |
private static class BindResult<T> { | |
private final BindType type; | |
private final Key<? extends T> key; | |
private final T instance; | |
private BindResult(BindType type, T instance, Key<? extends T> key) { | |
this.type = type; | |
this.instance = instance; | |
this.key = key; | |
} | |
@Override | |
public String toString() { | |
switch(type) { | |
case INSTANCE: | |
return "instance[" + instance + "]"; | |
case LINKED: | |
return "linkedKey[" + key + "]"; | |
case PROVIDER_INSTANCE: | |
return "providerInstance[" + instance + "]"; | |
} | |
return null; | |
} | |
} | |
private static class Visitor<T> extends | |
DefaultBindingTargetVisitor<T, Object> implements MultibindingsTargetVisitor<T, Object> { | |
public Object visit(MultibinderBinding<? extends T> multibinding) { | |
return multibinding; | |
} | |
public Object visit(MapBinderBinding<? extends T> mapbinding) { | |
return mapbinding; | |
} | |
} | |
} | |