Adding AssistedInject and ThrowingProviders as Guice extensions.
git-svn-id: https://google-guice.googlecode.com/svn/trunk@391 d779f126-a31b-0410-b53b-1d3aecad763e
diff --git a/extensions/assistedinject/assistedinject.iml b/extensions/assistedinject/assistedinject.iml
new file mode 100644
index 0000000..336fd39
--- /dev/null
+++ b/extensions/assistedinject/assistedinject.iml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module version="4" relativePaths="true" type="JAVA_MODULE">
+ <component name="ModuleRootManager" />
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
+ <exclude-output />
+ <content url="file://$MODULE_DIR$">
+ <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
+ </content>
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ <orderEntry type="module" module-name="guice" />
+ <orderEntry type="module-library">
+ <library>
+ <CLASSES>
+ <root url="jar://$MODULE_DIR$/../../lib/build/junit.jar!/" />
+ </CLASSES>
+ <JAVADOC />
+ <SOURCES />
+ </library>
+ </orderEntry>
+ <orderEntryProperties />
+ </component>
+</module>
+
diff --git a/extensions/assistedinject/build.properties b/extensions/assistedinject/build.properties
new file mode 100644
index 0000000..f7a30b2
--- /dev/null
+++ b/extensions/assistedinject/build.properties
@@ -0,0 +1,5 @@
+lib.dir=../../lib
+src.dir=src
+test.dir=test
+build.dir=build
+test.class=com.google.inject.assistedinject.FactoryProviderTest
diff --git a/extensions/assistedinject/build.xml b/extensions/assistedinject/build.xml
new file mode 100644
index 0000000..8e4b9bf
--- /dev/null
+++ b/extensions/assistedinject/build.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+
+<project name="guice-extensions-assistedinject" basedir="." default="jar">
+
+ <import file="../../common.xml"/>
+
+ <path id="compile.classpath">
+ <fileset dir="${lib.dir}" includes="*.jar"/>
+ <fileset dir="${lib.dir}/build" includes="*.jar"/>
+ <fileset dir="../../build/dist" includes="*.jar"/>
+ </path>
+
+ <target name="jar" depends="compile"
+ description="Build jar.">
+ <mkdir dir="${build.dir}"/>
+ <jar destfile="${build.dir}/${ant.project.name}-${version}.jar">
+ <fileset dir="${build.dir}/classes"/>
+ </jar>
+ </target>
+
+</project>
diff --git a/extensions/assistedinject/src/com/google/inject/assistedinject/Assisted.java b/extensions/assistedinject/src/com/google/inject/assistedinject/Assisted.java
new file mode 100755
index 0000000..4a24b86
--- /dev/null
+++ b/extensions/assistedinject/src/com/google/inject/assistedinject/Assisted.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (C) 2007 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.assistedinject;
+
+import static java.lang.annotation.ElementType.PARAMETER;
+import java.lang.annotation.Retention;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import java.lang.annotation.Target;
+
+/**
+ * The {@code @Assisted} annotation should be used on paramters within
+ * a constructor annotated with {@code @AssistedInject}. The annotation
+ * indicates that the parameter will be supplied through a factory
+ * method (the parameter will not be injected by Guice).
+ *
+ * @author jmourits@google.com (Jerome Mourits)
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+@Target({PARAMETER})
+@Retention(RUNTIME)
+public @interface Assisted {}
\ No newline at end of file
diff --git a/extensions/assistedinject/src/com/google/inject/assistedinject/AssistedConstructor.java b/extensions/assistedinject/src/com/google/inject/assistedinject/AssistedConstructor.java
new file mode 100755
index 0000000..4976bd0
--- /dev/null
+++ b/extensions/assistedinject/src/com/google/inject/assistedinject/AssistedConstructor.java
@@ -0,0 +1,102 @@
+/**
+ * Copyright (C) 2007 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.assistedinject;
+
+import com.google.inject.Inject;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Internal respresentation of a constructor annotated with
+ * {@link AssistedInject}
+ *
+ * @author jmourits@google.com (Jerome Mourits)
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+class AssistedConstructor<T> {
+
+ private final Constructor<T> constructor;
+ private final ParameterListKey assistedParameters;
+ private final List<Parameter> allParameters;
+
+ @SuppressWarnings("unchecked")
+ public AssistedConstructor(Constructor<T> constructor) {
+ this.constructor = constructor;
+
+ Type[] parameterTypes = constructor.getGenericParameterTypes();
+ Annotation[][] annotations = constructor.getParameterAnnotations();
+
+ List<Type> typeList = new ArrayList<Type>();
+ allParameters = new ArrayList<Parameter>();
+
+ // categorize params as @Assisted or @Injected
+ for (int i = 0; i < parameterTypes.length; i++) {
+ Parameter parameter = new Parameter(parameterTypes[i], annotations[i]);
+ allParameters.add(parameter);
+ if (parameter.isProvidedByFactory()) {
+ typeList.add(parameter.getType());
+ }
+ }
+ this.assistedParameters = new ParameterListKey(typeList);
+ }
+
+ /**
+ * Returns the {@link ParameterListKey} for this constructor. The
+ * {@link ParameterListKey} is created from the ordered list of {@link Assisted}
+ * constructor parameters.
+ */
+ public ParameterListKey getAssistedParameters() {
+ return assistedParameters;
+ }
+
+ /**
+ * Returns an ordered list of all constructor parameters (both
+ * {@link Assisted} and {@link Inject}ed).
+ */
+ public List<Parameter> getAllParameters() {
+ return allParameters;
+ }
+
+ public Set<Class<?>> getDeclaredExceptions() {
+ return new HashSet<Class<?>>(Arrays.asList(constructor.getExceptionTypes()));
+ }
+
+ /**
+ * Returns an instance of T, constructed using this constructor, with the
+ * supplied arguments.
+ */
+ public T newInstance(Object[] args) throws Throwable {
+ constructor.setAccessible(true);
+ try {
+ return constructor.newInstance(args);
+ } catch (InvocationTargetException e) {
+ throw e.getCause();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return constructor.toString();
+ }
+}
diff --git a/extensions/assistedinject/src/com/google/inject/assistedinject/AssistedInject.java b/extensions/assistedinject/src/com/google/inject/assistedinject/AssistedInject.java
new file mode 100755
index 0000000..a936dd4
--- /dev/null
+++ b/extensions/assistedinject/src/com/google/inject/assistedinject/AssistedInject.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright (C) 2007 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.assistedinject;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import java.lang.annotation.Retention;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import java.lang.annotation.Target;
+
+/**
+ * Constructors annotated with <code>@AssistedInject</code> indicate that will
+ * can be instantiated by the {@link FactoryProvider}. Each constructor must
+ * exactly one corresponding factory method within the Factory Interface.
+ *
+ * <p>Constructor parameters must be either supplied by the Factory Interface and
+ * marked with <code>@Assisted</code>, or they must be injectable.
+ *
+ * @author jmourits@google.com (Jerome Mourits)
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+@Target({CONSTRUCTOR})
+@Retention(RUNTIME)
+public @interface AssistedInject {}
\ No newline at end of file
diff --git a/extensions/assistedinject/src/com/google/inject/assistedinject/FactoryProvider.java b/extensions/assistedinject/src/com/google/inject/assistedinject/FactoryProvider.java
new file mode 100755
index 0000000..2396133
--- /dev/null
+++ b/extensions/assistedinject/src/com/google/inject/assistedinject/FactoryProvider.java
@@ -0,0 +1,241 @@
+/**
+ * Copyright (C) 2007 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.assistedinject;
+
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Provides a mechanism to combine user-specified paramters with
+ * {@link Injector}-specified paramters when creating new objects.
+ *
+ * <p>To use a {@link FactoryProvider}:
+ *
+ * <p>Annotate your implementation class' constructor with the
+ * {@link @AssistedInject} and the user-specified parameters with
+ * {@link @Assisted}:
+ * <pre><code>public class RealPayment implements Payment {
+ * @AssistedInject
+ * public RealPayment(CreditService creditService, AuthService authService,
+ * @Assisted Date startDate, @Assisted Money amount) {
+ * ...
+ * }
+ * }</code></pre>
+ *
+ * <p>Write an interface with a <i>create</i> method that accepts the user-specified
+ * parameters in the same order as they appear in the implementation class' constructor:
+ * <pre><code>public interface PaymentFactory {
+ * Payment create(Date startDate, Money amount);
+ * }</code></pre>
+ *
+ * <p>You can name your create methods whatever you like, such as <i>create</i>,
+ * or <i>createPayment</i> or <i>newPayment</i>. The concrete class must
+ * be assignable to the return type of your create method. You can also provide
+ * multiple factory methods, but there must be exactly one {@link @AssistedInject}
+ * constructor on the implementation class for each.
+ *
+ * <p>In your Guice {@link com.google.inject.Module module}, bind your factory
+ * interface to an instance of {@link FactoryProvider} that was created with
+ * the same factory interface and implementation type:
+ * <pre><code> bind(PaymentFactory.class).toProvider(
+ * FactoryProvider.newFactory(PaymentFactory.class, RealPayment.class));</code></pre>
+ *
+ * <p>Now you can {@code @Inject} your factory interface into your Guice-injected
+ * classes. When you invoke the create method on that factory, the
+ * {@link FactoryProvider} will instantiate the implementation class using
+ * parameters from the injector and the factory method.
+ *
+ * <pre><code>public class PaymentAction {
+ * @Inject private PaymentFactory paymentFactory;
+ *
+ * public void doPayment(Money amount) {
+ * Payment payment = paymentFactory.create(new Date(), amount);
+ * payment.apply();
+ * }
+ * }
+ *
+ * @typeparam F The factory interface
+ * @typeparam R The concrete class to be created.
+ *
+ * @author jmourits@google.com (Jerome Mourits)
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public class FactoryProvider<F, R> implements Provider<F> {
+
+ private Injector injector;
+
+ private final Class<F> factoryType;
+ private final Class<R> implementationType;
+ private final Map<Method, AssistedConstructor<?>> factoryMethodToConstructor;
+
+ public static <X,Y> FactoryProvider<X,Y> newFactory(
+ Class<X> factoryType, Class<Y> implementationType){
+ return new FactoryProvider<X, Y>(factoryType,implementationType);
+ }
+
+ private FactoryProvider(Class<F> factoryType, Class<R> implementationType) {
+ this.factoryType = factoryType;
+ this.implementationType = implementationType;
+ this.factoryMethodToConstructor = createMethodMapping();
+ checkDeclaredExceptionsMatch();
+ }
+
+ @Inject
+ @SuppressWarnings({"unchecked", "unused"})
+ private void setInjectorAndCheckUnboundParametersAreInjectable(
+ Injector injector) {
+ this.injector = injector;
+ for (AssistedConstructor<?> c : factoryMethodToConstructor.values()) {
+ for (Parameter p : c.getAllParameters()) {
+ if(!p.isProvidedByFactory() && !paramCanBeInjected(p, injector)) {
+ throw new IllegalStateException(String.format(
+ "Parameter of type '%s' is not injectable or annotated "
+ + "with @Assisted for Constructor '%s'", p, c));
+ }
+ }
+ }
+ }
+
+ private void checkDeclaredExceptionsMatch() {
+ for (Map.Entry<Method, AssistedConstructor<?>> entry : factoryMethodToConstructor.entrySet()) {
+ for (Class<?> constructorException : entry.getValue().getDeclaredExceptions()) {
+ if (!isConstructorExceptionCompatibleWithFactoryExeception(
+ constructorException, entry.getKey().getExceptionTypes())) {
+ throw new IllegalStateException(String.format(
+ "Constructor %s declares an exception, but no compatible exception is thrown "
+ + "by the factory method %s", entry.getValue(), entry.getKey()));
+ }
+ }
+ }
+ }
+
+ private boolean isConstructorExceptionCompatibleWithFactoryExeception(
+ Class<?> constructorException, Class<?>[] factoryExceptions) {
+ for (Class<?> factoryException : factoryExceptions) {
+ if (factoryException.isAssignableFrom(constructorException)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @SuppressWarnings("unchecked")
+ private boolean paramCanBeInjected(Parameter parameter, Injector injector) {
+ return parameter.isBound(injector);
+ }
+
+ @SuppressWarnings({"unchecked"})
+ private Map<Method, AssistedConstructor<?>> createMethodMapping() {
+
+ List<AssistedConstructor<?>> constructors = new ArrayList<AssistedConstructor<?>>();
+
+ for (Constructor c : implementationType.getDeclaredConstructors()) {
+ if (c.getAnnotation(AssistedInject.class) != null) {
+ constructors.add(new AssistedConstructor(c));
+ }
+ }
+
+ if (constructors.size() != factoryType.getMethods().length) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Constructor mismatch: %s has %s @AssistedInject " +
+ "constructors, factory %s has %s creation methods",
+ implementationType.getSimpleName(),
+ constructors.size(),
+ factoryType.getSimpleName(),
+ factoryType.getMethods().length));
+ }
+
+ Map<ParameterListKey, AssistedConstructor> paramsToConstructor
+ = new HashMap<ParameterListKey, AssistedConstructor>();
+
+ for (AssistedConstructor c : constructors) {
+ if (paramsToConstructor.containsKey(c.getAssistedParameters())) {
+ throw new RuntimeException("Duplicate constructor, " + c);
+ }
+ paramsToConstructor.put(c.getAssistedParameters(), c);
+ }
+
+ Map<Method, AssistedConstructor<?>> result = new HashMap<Method, AssistedConstructor<?>>();
+ for (Method method : factoryType.getMethods()) {
+ if (!method.getReturnType().isAssignableFrom(implementationType)) {
+ throw new RuntimeException(String.format("Return type of method \"%s\""
+ + " is not assignable from class \"%s\"", method,
+ implementationType.getName()));
+ }
+ ParameterListKey methodParams = new ParameterListKey(method.getGenericParameterTypes());
+
+ if (!paramsToConstructor.containsKey(methodParams)) {
+ throw new IllegalArgumentException(String.format("%s has no " +
+ "@AssistInject constructor that takes the @Assisted parameters %s " +
+ "in that order. @AssistInject constructors are %s",
+ implementationType, methodParams, paramsToConstructor.values()));
+ }
+ AssistedConstructor matchingConstructor = paramsToConstructor.remove(methodParams);
+
+ result.put(method, matchingConstructor);
+ }
+ return result;
+ }
+
+ @SuppressWarnings({"unchecked"})
+ public F get() {
+ InvocationHandler invocationHandler = new InvocationHandler() {
+
+ public Object invoke(Object proxy, Method method, Object[] creationArgs) throws Throwable {
+ AssistedConstructor<?> constructor = factoryMethodToConstructor.get(method);
+
+ Object[] constructorArgs = gatherArgsForConstructor(
+ constructor, creationArgs);
+ Object objectToReturn = constructor.newInstance(constructorArgs);
+ injector.injectMembers(objectToReturn);
+ return objectToReturn;
+ }
+
+ public Object[] gatherArgsForConstructor(
+ AssistedConstructor<?> constructor,
+ Object[] factoryArgs) {
+ int numParams = constructor.getAllParameters().size();
+ int argPosition = 0;
+ Object[] result = new Object[numParams];
+
+ for (int i = 0; i < numParams; i++) {
+ Parameter parameter = constructor.getAllParameters().get(i);
+ if (parameter.isProvidedByFactory()) {
+ result[i] = factoryArgs[argPosition];
+ argPosition++;
+ } else {
+ result[i] = parameter.getValue(injector);
+ }
+ }
+ return result;
+ }
+ };
+
+ return (F) Proxy.newProxyInstance(factoryType.getClassLoader(),
+ new Class[] {factoryType}, invocationHandler);
+ }
+}
diff --git a/extensions/assistedinject/src/com/google/inject/assistedinject/Parameter.java b/extensions/assistedinject/src/com/google/inject/assistedinject/Parameter.java
new file mode 100755
index 0000000..a4934c8
--- /dev/null
+++ b/extensions/assistedinject/src/com/google/inject/assistedinject/Parameter.java
@@ -0,0 +1,185 @@
+/**
+ * Copyright (C) 2007 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.assistedinject;
+
+import com.google.inject.BindingAnnotation;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Models a method or constructor parameter.
+ *
+ * @author jmourits@google.com (Jerome Mourits)
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+class Parameter {
+
+ private static final Map<Type, Type> PRIMITIVE_COUNTERPARTS;
+ static {
+ Map<Type, Type> primitiveToWrapper = new HashMap<Type, Type>() {{
+ put(int.class, Integer.class);
+ put(long.class, Long.class);
+ put(boolean.class, Boolean.class);
+ put(byte.class, Byte.class);
+ put(short.class, Short.class);
+ put(float.class, Float.class);
+ put(double.class, Double.class);
+ put(char.class, Character.class);
+ }};
+
+ Map<Type, Type> counterparts = new HashMap<Type, Type>();
+ for (Map.Entry<Type, Type> entry : primitiveToWrapper.entrySet()) {
+ Type key = entry.getKey();
+ Type value = entry.getValue();
+ counterparts.put(key, value);
+ counterparts.put(value, key);
+ }
+
+ PRIMITIVE_COUNTERPARTS = Collections.unmodifiableMap(counterparts);
+ }
+
+ private final Type type;
+ private final boolean isAssisted;
+ private final Annotation bindingAnnotation;
+ private final boolean isProvider;
+
+ public Parameter(Type type, Annotation[] annotations) {
+ this.type = type;
+ this.bindingAnnotation = getBindingAnnotation(annotations);
+ this.isAssisted = hasAssistedAnnotation(annotations);
+ this.isProvider = isProvider(type);
+ }
+
+ public boolean isProvidedByFactory() {
+ return isAssisted;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ if (isAssisted) {
+ result.append("@Assisted");
+ result.append(" ");
+ }
+ if (bindingAnnotation != null) {
+ result.append(bindingAnnotation.toString());
+ result.append(" ");
+ }
+ result.append(type.toString());
+ return result.toString();
+ }
+
+ private boolean hasAssistedAnnotation(Annotation[] annotations) {
+ for (Annotation annotation : annotations) {
+ if (annotation.annotationType().equals(Assisted.class)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the Guice {@link Key} for this parameter.
+ */
+ public Object getValue(Injector injector) {
+ return isProvider
+ ? injector.getProvider(getBindingForType(getProvidedType(type)))
+ : injector.getInstance(getPrimaryBindingKey());
+ }
+
+ public boolean isBound(Injector injector) {
+ return injector.getBinding(getPrimaryBindingKey()) != null
+ || injector.getBinding(getAlternateGuiceBindingKey()) != null
+ || injector.getBinding(fixAnnotations(getPrimaryBindingKey())) != null
+ || injector.getBinding(fixAnnotations(getAlternateGuiceBindingKey())) != null;
+ }
+
+ /**
+ * Replace annotation instances with annotation types, this is only
+ * appropriate for testing if a key is bound and not for injecting.
+ *
+ * See Guice bug 125,
+ * http://code.google.com/p/google-guice/issues/detail?id=125
+ */
+ public Key<?> fixAnnotations(Key<?> key) {
+ return key.getAnnotation() == null
+ ? key
+ : Key.get(key.getTypeLiteral(), key.getAnnotation().annotationType());
+ }
+
+ private Key<?> getPrimaryBindingKey() {
+ return isProvider
+ ? getBindingForType(getProvidedType(type))
+ : getBindingForType(type);
+ }
+
+ private Key<?> getAlternateGuiceBindingKey() {
+ Type counterpart = (PRIMITIVE_COUNTERPARTS.containsKey(type))
+ ? PRIMITIVE_COUNTERPARTS.get(type)
+ : type;
+ return isProvider
+ ? getBindingForType(getProvidedType(counterpart))
+ : getBindingForType(counterpart);
+ }
+
+
+ private Type getProvidedType(Type type) {
+ return ((ParameterizedType)type).getActualTypeArguments()[0];
+ }
+
+ private boolean isProvider(Type type) {
+ return type instanceof ParameterizedType
+ && ((ParameterizedType)type).getRawType() == Provider.class;
+ }
+
+ private Key<?> getBindingForType(Type type) {
+ return bindingAnnotation != null
+ ? Key.get(type, bindingAnnotation)
+ : Key.get(type);
+ }
+
+ /**
+ * Returns the unique binding annotation from the specified list, or
+ * {@code null} if there are none.
+ *
+ * @throws IllegalStateException if multiple binding annotations exist.
+ */
+ private Annotation getBindingAnnotation(Annotation[] annotations) {
+ Annotation bindingAnnotation = null;
+ for (Annotation a : annotations) {
+ if (a.annotationType().getAnnotation(BindingAnnotation.class) != null) {
+ if (bindingAnnotation != null) {
+ throw new IllegalArgumentException(String.format("Parameter has " +
+ "multiple binding annotations: %s and %s", bindingAnnotation, a));
+ }
+ bindingAnnotation = a;
+ }
+ }
+ return bindingAnnotation;
+ }
+}
\ No newline at end of file
diff --git a/extensions/assistedinject/src/com/google/inject/assistedinject/ParameterListKey.java b/extensions/assistedinject/src/com/google/inject/assistedinject/ParameterListKey.java
new file mode 100755
index 0000000..c0e3274
--- /dev/null
+++ b/extensions/assistedinject/src/com/google/inject/assistedinject/ParameterListKey.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (C) 2007 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.assistedinject;
+
+import com.google.inject.TypeLiteral;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A list of {@link TypeLiteral}s to match an injectable Constructor's assited
+ * parameter types to the corresponding factory method.
+ *
+ * @author jmourits@google.com (Jerome Mourits)
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+class ParameterListKey {
+
+ private final List<Type> paramList;
+
+ public ParameterListKey(List<Type> paramList) {
+ this.paramList = new ArrayList<Type>(paramList);
+ }
+
+ public ParameterListKey(Type[] types) {
+ this(Arrays.asList(types));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof ParameterListKey)) {
+ return false;
+ }
+ ParameterListKey other = (ParameterListKey) o;
+ return paramList.equals(other.paramList);
+ }
+
+ @Override
+ public int hashCode() {
+ return paramList.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return paramList.toString();
+ }
+}
\ No newline at end of file
diff --git a/extensions/assistedinject/test/com/google/inject/assistedinject/FactoryProviderTest.java b/extensions/assistedinject/test/com/google/inject/assistedinject/FactoryProviderTest.java
new file mode 100755
index 0000000..6f547f3
--- /dev/null
+++ b/extensions/assistedinject/test/com/google/inject/assistedinject/FactoryProviderTest.java
@@ -0,0 +1,475 @@
+/**
+ * Copyright (C) 2007 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.assistedinject;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+import java.awt.Color;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+import junit.framework.TestCase;
+
+/**
+ * @author jmourits@google.com (Jerome Mourits)
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public class FactoryProviderTest extends TestCase {
+
+ public void testAssistedFactory() {
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(Double.class).toInstance(5.0d);
+ bind(ColoredCarFactory.class)
+ .toProvider(FactoryProvider.newFactory(ColoredCarFactory.class, Mustang.class));
+ }
+ });
+ ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class);
+
+ Mustang blueMustang = (Mustang) carFactory.create(Color.BLUE);
+ assertEquals(Color.BLUE, blueMustang.color);
+ assertEquals(5.0d, blueMustang.engineSize);
+
+ Mustang redMustang = (Mustang) carFactory.create(Color.RED);
+ assertEquals(Color.RED, redMustang.color);
+ assertEquals(5.0d, redMustang.engineSize);
+ }
+
+ public void testAssistedFactoryWithAnnotations() {
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(int.class).annotatedWith(Names.named("horsePower")).toInstance(250);
+ bind(int.class).annotatedWith(Names.named("modelYear")).toInstance(1984);
+ bind(ColoredCarFactory.class)
+ .toProvider(FactoryProvider.newFactory(ColoredCarFactory.class, Camaro.class));
+ }
+ });
+
+ ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class);
+
+ Camaro blueCamaro = (Camaro) carFactory.create(Color.BLUE);
+ assertEquals(Color.BLUE, blueCamaro.color);
+ assertEquals(1984, blueCamaro.modelYear);
+ assertEquals(250, blueCamaro.horsePower);
+
+ Camaro redCamaro = (Camaro) carFactory.create(Color.RED);
+ assertEquals(Color.RED, redCamaro.color);
+ assertEquals(1984, redCamaro.modelYear);
+ assertEquals(250, redCamaro.horsePower);
+ }
+
+ interface Car {
+ }
+
+ interface ColoredCarFactory {
+ Car create(Color color);
+ }
+
+ public static class Mustang implements Car {
+ private final double engineSize;
+ private final Color color;
+
+ @AssistedInject
+ public Mustang(double engineSize, @Assisted Color color) {
+ this.engineSize = engineSize;
+ this.color = color;
+ }
+ }
+
+ public static class Camaro implements Car {
+ private final int horsePower;
+ private final int modelYear;
+ private final Color color;
+
+ @AssistedInject
+ public Camaro(
+ @Named("horsePower")int horsePower,
+ @Named("modelYear")int modelYear,
+ @Assisted Color color) {
+ this.horsePower = horsePower;
+ this.modelYear = modelYear;
+ this.color = color;
+ }
+ }
+
+ interface SummerCarFactory {
+ Car create(Color color, boolean convertable);
+ Car createConvertible(Color color);
+ }
+
+ public void testFactoryWithMultipleMethods() {
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(float.class).toInstance(140f);
+ bind(SummerCarFactory.class).toProvider(
+ FactoryProvider.newFactory(SummerCarFactory.class, Corvette.class));
+ }
+ });
+
+ SummerCarFactory carFactory = injector.getInstance(SummerCarFactory.class);
+
+ Corvette blueCorvette = (Corvette) carFactory.createConvertible(Color.BLUE);
+ assertEquals(Color.BLUE, blueCorvette.color);
+ assertEquals(100f, blueCorvette.maxMph);
+ assertTrue(blueCorvette.isConvertable);
+
+ Corvette redCorvette = (Corvette) carFactory.create(Color.RED, false);
+ assertEquals(Color.RED, redCorvette.color);
+ assertEquals(140f, redCorvette.maxMph);
+ assertFalse(redCorvette.isConvertable);
+ }
+
+ public static class Corvette implements Car {
+ private boolean isConvertable;
+ private Color color;
+ private float maxMph;
+
+ @AssistedInject
+ public Corvette(@Assisted Color color) {
+ this(color, 100f, true);
+ }
+
+ public Corvette(@Assisted Color color, @Assisted boolean isConvertable) {
+ throw new IllegalStateException("Not an @AssistedInject constructor");
+ }
+
+ @AssistedInject
+ public Corvette(@Assisted Color color, Float maxMph, @Assisted boolean isConvertable) {
+ this.isConvertable = isConvertable;
+ this.color = color;
+ this.maxMph = maxMph;
+ }
+ }
+
+ public void testFactoryMethodsMismatch() {
+ try {
+ FactoryProvider.newFactory(SummerCarFactory.class, Beetle.class);
+ fail();
+ } catch(IllegalArgumentException e) {
+ assertTrue(e.getMessage().startsWith("Constructor mismatch"));
+ }
+ }
+
+ public static class Beetle implements Car {
+ @AssistedInject
+ public Beetle(@Assisted Color color) {
+ throw new IllegalStateException("Conflicting constructors");
+ }
+ @AssistedInject
+ public Beetle(@Assisted Color color, @Assisted boolean isConvertable) {
+ throw new IllegalStateException("Conflicting constructors");
+ }
+ @AssistedInject
+ public Beetle(@Assisted Color color, @Assisted boolean isConvertable, float maxMph) {
+ throw new IllegalStateException("Conflicting constructors");
+ }
+ }
+
+ public void testMethodsAndFieldsGetInjected() {
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(String.class).toInstance("turbo");
+ bind(int.class).toInstance(911);
+ bind(double.class).toInstance(50000d);
+ bind(ColoredCarFactory.class)
+ .toProvider(FactoryProvider.newFactory(ColoredCarFactory.class, Porshe.class));
+ }
+ });
+ ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class);
+
+ Porshe grayPorshe = (Porshe) carFactory.create(Color.GRAY);
+ assertEquals(Color.GRAY, grayPorshe.color);
+ assertEquals(50000d, grayPorshe.price);
+ assertEquals(911, grayPorshe.model);
+ assertEquals("turbo", grayPorshe.name);
+ }
+
+ public static class Porshe implements Car {
+ private final Color color;
+ private final double price;
+ private @Inject String name;
+ private int model;
+
+ @AssistedInject
+ public Porshe(@Assisted Color color, double price) {
+ this.color = color;
+ this.price = price;
+ }
+
+ @Inject void setModel(int model) {
+ this.model = model;
+ }
+ }
+
+ public void testProviderInjection() {
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(String.class).toInstance("trans am");
+ bind(ColoredCarFactory.class)
+ .toProvider(FactoryProvider.newFactory(ColoredCarFactory.class, Firebird.class));
+ }
+ });
+ ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class);
+
+ Firebird blackFirebird = (Firebird) carFactory.create(Color.BLACK);
+ assertEquals(Color.BLACK, blackFirebird.color);
+ assertEquals("trans am", blackFirebird.modifiersProvider.get());
+ }
+
+ public static class Firebird implements Car {
+ private final Provider<String> modifiersProvider;
+ private final Color color;
+
+ @AssistedInject
+ public Firebird(Provider<String> modifiersProvider, @Assisted Color color) {
+ this.modifiersProvider = modifiersProvider;
+ this.color = color;
+ }
+ }
+
+ public void testTypeTokenInjection() {
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(new TypeLiteral<Set<String>>() {}).toInstance(Collections.singleton("Flux Capacitor"));
+ bind(new TypeLiteral<Set<Integer>>() {}).toInstance(Collections.singleton(88));
+ bind(ColoredCarFactory.class)
+ .toProvider(FactoryProvider.newFactory(ColoredCarFactory.class, DeLorean.class));
+ }
+ });
+ ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class);
+
+ DeLorean deLorean = (DeLorean) carFactory.create(Color.GRAY);
+ assertEquals(Color.GRAY, deLorean.color);
+ assertEquals("Flux Capacitor", deLorean.features.iterator().next());
+ assertEquals(new Integer(88), deLorean.featureActivationSpeeds.iterator().next());
+ }
+
+ public static class DeLorean implements Car {
+ private final Set<String> features;
+ private final Set<Integer> featureActivationSpeeds;
+ private final Color color;
+
+ @AssistedInject
+ public DeLorean(
+ Set<String> extraFeatures, Set<Integer> featureActivationSpeeds, @Assisted Color color) {
+ this.features = extraFeatures;
+ this.featureActivationSpeeds = featureActivationSpeeds;
+ this.color = color;
+ }
+ }
+
+ public void testTypeTokenProviderInjection() {
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(new TypeLiteral<Set<String>>() { }).toInstance(Collections.singleton("Datsun"));
+ bind(ColoredCarFactory.class)
+ .toProvider(FactoryProvider.newFactory(ColoredCarFactory.class, Z.class));
+ }
+ });
+ ColoredCarFactory carFactory = injector.getInstance(ColoredCarFactory.class);
+
+ Z orangeZ = (Z) carFactory.create(Color.ORANGE);
+ assertEquals(Color.ORANGE, orangeZ.color);
+ assertEquals("Datsun", orangeZ.manufacturersProvider.get().iterator().next());
+ }
+
+ public static class Z implements Car {
+ private final Provider<Set<String>> manufacturersProvider;
+ private final Color color;
+
+ @AssistedInject
+ public Z(Provider<Set<String>> manufacturersProvider, @Assisted Color color) {
+ this.manufacturersProvider = manufacturersProvider;
+ this.color = color;
+ }
+ }
+
+ public static class Prius implements Car {
+ @SuppressWarnings("unused")
+ private final Color color;
+
+ @AssistedInject
+ private Prius(@Assisted Color color) {
+ this.color = color;
+ }
+ }
+
+ public void testAssistInjectionInNonPublicConstructor() {
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(ColoredCarFactory.class)
+ .toProvider(FactoryProvider.newFactory(ColoredCarFactory.class, Prius.class));
+ }
+ });
+ Car car = injector.getInstance(ColoredCarFactory.class).create(Color.ORANGE);
+ }
+
+ public static class ExplodingCar implements Car {
+ @AssistedInject
+ public ExplodingCar(@Assisted Color color) {
+ throw new IllegalStateException("kaboom!");
+ }
+ }
+
+ public void testExceptionDuringConstruction() {
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(ColoredCarFactory.class).toProvider(
+ FactoryProvider.newFactory(ColoredCarFactory.class, ExplodingCar.class));
+ }
+ });
+ try {
+ injector.getInstance(ColoredCarFactory.class).create(Color.ORANGE);
+ fail();
+ } catch (IllegalStateException e) {
+ assertEquals("kaboom!", e.getMessage());
+ }
+ }
+
+ public static class DefectiveCar implements Car {
+ @AssistedInject
+ public DefectiveCar() throws ExplosionException, FireException {
+ throw new ExplosionException();
+ }
+ }
+
+ public static class ExplosionException extends Exception { }
+ public static class FireException extends Exception { }
+
+ public interface DefectiveCarFactoryWithNoExceptions {
+ Car createCar();
+ }
+
+ public interface DefectiveCarFactory {
+ Car createCar() throws FireException;
+ }
+
+ public void testFactoryMethodMustDeclareAllConstructorExceptions() {
+ try {
+ FactoryProvider.newFactory(DefectiveCarFactoryWithNoExceptions.class, DefectiveCar.class);
+ fail();
+ } catch (IllegalStateException e) {
+ assertTrue(e.getMessage().contains("no compatible exception is thrown"));
+ }
+ }
+
+ public interface CorrectDefectiveCarFactory {
+ Car createCar() throws FireException, ExplosionException;
+ }
+
+ public void testConstructorExceptionsAreThrownByFactory() {
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(CorrectDefectiveCarFactory.class).toProvider(
+ FactoryProvider.newFactory(
+ CorrectDefectiveCarFactory.class, DefectiveCar.class));
+ }
+ });
+ try {
+ injector.getInstance(CorrectDefectiveCarFactory.class).createCar();
+ fail();
+ } catch (FireException e) {
+ fail();
+ } catch (ExplosionException expected) {
+ }
+ }
+
+ public static class MultipleConstructorDefectiveCar implements Car {
+ @AssistedInject
+ public MultipleConstructorDefectiveCar() throws ExplosionException {
+ throw new ExplosionException();
+ }
+
+ @AssistedInject
+ public MultipleConstructorDefectiveCar(@Assisted Color c) throws FireException {
+ throw new FireException();
+ }
+ }
+
+ public interface MultipleConstructorDefectiveCarFactory {
+ Car createCar() throws ExplosionException;
+ Car createCar(Color r) throws FireException;
+ }
+
+ public void testMultipleConstructorExceptionMatching() {
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(MultipleConstructorDefectiveCarFactory.class).toProvider(
+ FactoryProvider.newFactory(
+ MultipleConstructorDefectiveCarFactory.class,
+ MultipleConstructorDefectiveCar.class));
+ }
+ });
+ MultipleConstructorDefectiveCarFactory factory
+ = injector.getInstance(MultipleConstructorDefectiveCarFactory.class);
+ try {
+ factory.createCar();
+ fail();
+ } catch (ExplosionException expected) {
+ }
+
+ try {
+ factory.createCar(Color.RED);
+ fail();
+ } catch (FireException expected) {
+ }
+ }
+
+ public static class WildcardCollection {
+
+ public interface Factory {
+ WildcardCollection create(Collection<?> items);
+ }
+
+ @AssistedInject
+ public WildcardCollection(@Assisted Collection<?> items) { }
+ }
+
+ public void testWildcardGenerics() {
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(WildcardCollection.Factory.class).toProvider(
+ FactoryProvider.newFactory(
+ WildcardCollection.Factory.class,
+ WildcardCollection.class));
+ }
+ });
+ WildcardCollection.Factory factory = injector.getInstance(WildcardCollection.Factory.class);
+ factory.create(Collections.emptyList());
+ }
+
+
+ // TODO(jessewilson): test for duplicate constructors
+}
\ No newline at end of file
diff --git a/extensions/throwingproviders/build.properties b/extensions/throwingproviders/build.properties
new file mode 100644
index 0000000..0222b47
--- /dev/null
+++ b/extensions/throwingproviders/build.properties
@@ -0,0 +1,5 @@
+lib.dir=../../lib
+src.dir=src
+test.dir=test
+build.dir=build
+test.class=com.google.inject.throwingproviders.ThrowingProviderBinderTest
diff --git a/extensions/throwingproviders/build.xml b/extensions/throwingproviders/build.xml
new file mode 100644
index 0000000..b324cfb
--- /dev/null
+++ b/extensions/throwingproviders/build.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+
+<project name="guice-extensions-throwingproviders" basedir="." default="jar">
+
+ <import file="../../common.xml"/>
+
+ <path id="compile.classpath">
+ <fileset dir="${lib.dir}" includes="*.jar"/>
+ <fileset dir="${lib.dir}/build" includes="*.jar"/>
+ <fileset dir="../../build/dist" includes="*.jar"/>
+ </path>
+
+ <target name="jar" depends="compile"
+ description="Build jar.">
+ <mkdir dir="${build.dir}"/>
+ <jar destfile="${build.dir}/${ant.project.name}-${version}.jar">
+ <fileset dir="${build.dir}/classes"/>
+ </jar>
+ </target>
+
+</project>
diff --git a/extensions/throwingproviders/src/com/google/inject/throwingproviders/ThrowingProvider.java b/extensions/throwingproviders/src/com/google/inject/throwingproviders/ThrowingProvider.java
new file mode 100644
index 0000000..a4189cf
--- /dev/null
+++ b/extensions/throwingproviders/src/com/google/inject/throwingproviders/ThrowingProvider.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright (C) 2007 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.throwingproviders;
+
+/**
+ * Alternative to the Guice {@link com.google.inject.Provider} that throws
+ * a checked Exception. Users may not inject {@code T} directly.
+ *
+ * <p>This interface must be extended to use application-specific exception types.
+ * Such subinterfaces may not define new methods:
+ * <pre>
+ * public interface RemoteProvider<T> extends ThrowingProvider<T, RemoteException> { }
+ * </pre>
+ *
+ * <p>When this type is bound using {@link ThrowingProviderBinder}, the value returned
+ * or exception thrown by {@link #get} will be scoped. As a consequence, {@link #get}
+ * will invoked at most once within each scope.
+ *
+ * @author jmourits@google.com (Jerome Mourits)
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public interface ThrowingProvider<T,E extends Exception> {
+ T get() throws E;
+}
diff --git a/extensions/throwingproviders/src/com/google/inject/throwingproviders/ThrowingProviderBinder.java b/extensions/throwingproviders/src/com/google/inject/throwingproviders/ThrowingProviderBinder.java
new file mode 100644
index 0000000..0dc7cae
--- /dev/null
+++ b/extensions/throwingproviders/src/com/google/inject/throwingproviders/ThrowingProviderBinder.java
@@ -0,0 +1,293 @@
+/**
+ * Copyright (C) 2007 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.throwingproviders;
+
+import com.google.inject.Binder;
+import com.google.inject.BindingAnnotation;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
+import com.google.inject.binder.ScopedBindingBuilder;
+import com.google.inject.internal.Objects;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Proxy;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+
+/**
+ * <p>Builds a binding for an {@link ThrowingProvider} using a fluent API.
+ * For example:
+ * <pre>
+ * ThrowingProviderBinder.create(binder())
+ * .bind(RemoteProvider.class, Customer.class)
+ * .to(RemoteCustomerProvider.class)
+ * .in(RequestScope.class);
+ * </pre>
+ *
+ * @author jmourits@google.com (Jerome Mourits)
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public class ThrowingProviderBinder {
+
+ private final Binder binder;
+
+ private ThrowingProviderBinder(Binder binder) {
+ this.binder = binder;
+ }
+
+ public static ThrowingProviderBinder create(Binder binder) {
+ return new ThrowingProviderBinder(binder);
+ }
+
+ public <P extends ThrowingProvider> SecondaryBinder<P>
+ bind(final Class<P> interfaceType, final Type valueType) {
+ return new SecondaryBinder<P>(interfaceType, valueType);
+ }
+
+ public class SecondaryBinder<P extends ThrowingProvider> {
+ private final Class<P> interfaceType;
+ private final Type valueType;
+ private Class<? extends Annotation> annotationType;
+ private Annotation annotation;
+ private final Class<?> exceptionType;
+
+ public SecondaryBinder(Class<P> interfaceType, Type valueType) {
+ this.interfaceType = Objects.nonNull(interfaceType, "interfaceType");
+ this.valueType = Objects.nonNull(valueType, "valueType");
+ checkInterface();
+ this.exceptionType = getExceptionType(interfaceType);
+ }
+
+ public SecondaryBinder<P> annotatedWith(Class<? extends Annotation> annotationType) {
+ if (!(this.annotationType == null && this.annotation == null)) {
+ throw new IllegalStateException();
+ }
+ this.annotationType = annotationType;
+ return this;
+ }
+
+ public SecondaryBinder<P> annotatedWith(Annotation annotation) {
+ if (!(this.annotationType == null && this.annotation == null)) {
+ throw new IllegalStateException();
+ }
+ this.annotation = annotation;
+ return this;
+ }
+
+ public ScopedBindingBuilder to(P target) {
+ Key<P> targetKey = Key.get(interfaceType, uniqueAnnotation());
+ binder.bind(targetKey).toInstance(target);
+ return to(targetKey);
+ }
+
+ public ScopedBindingBuilder to(Class<? extends P> targetType) {
+ return to(Key.get(targetType));
+ }
+
+ public ScopedBindingBuilder to(final Key<? extends P> targetKey) {
+ Objects.nonNull(targetKey, "targetKey");
+ final Key<Result> resultKey = Key.get(Result.class, uniqueAnnotation());
+ final Key<P> key = createKey();
+
+ binder.bind(key).toProvider(new Provider<P>() {
+ private P instance;
+
+ @Inject void initialize(final Injector injector) {
+ instance = interfaceType.cast(Proxy.newProxyInstance(
+ interfaceType.getClassLoader(), new Class<?>[] { interfaceType },
+ new InvocationHandler() {
+ public Object invoke(Object proxy, Method method, Object[] args)
+ throws Throwable {
+ return injector.getInstance(resultKey).getOrThrow();
+ }
+ }));
+ }
+
+ public P get() {
+ return instance;
+ }
+ });
+
+ return binder.bind(resultKey).toProvider(new Provider<Result>() {
+ private Injector injector;
+
+ @Inject void initialize(Injector injector) {
+ this.injector = injector;
+ }
+
+ public Result get() {
+ try {
+ return Result.forValue(injector.getInstance(targetKey).get());
+ } catch (Exception e) {
+ if (exceptionType.isInstance(e)) {
+ return Result.forException(e);
+ } else if (e instanceof RuntimeException) {
+ throw (RuntimeException) e;
+ } else {
+ // this should never happen
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Returns the exception type declared to be thrown by the get method of
+ * {@code interfaceType}.
+ */
+ @SuppressWarnings({"unchecked"})
+ private <P extends ThrowingProvider> Class<?> getExceptionType(Class<P> interfaceType) {
+ ParameterizedType genericUnreliableProvider
+ = (ParameterizedType) interfaceType.getGenericInterfaces()[0];
+ return (Class<? extends Exception>) genericUnreliableProvider.getActualTypeArguments()[1];
+ }
+
+ private void checkInterface() {
+ String errorMessage = "%s is not a compliant interface "
+ + "- see the Javadoc for ThrowingProvider";
+
+ checkArgument(interfaceType.isInterface(), errorMessage, interfaceType.getName());
+ checkArgument(interfaceType.getGenericInterfaces().length == 1, errorMessage,
+ interfaceType.getName());
+ checkArgument(interfaceType.getInterfaces()[0] == ThrowingProvider.class,
+ errorMessage, interfaceType.getName());
+
+ // Ensure that T is parameterized and unconstrained.
+ ParameterizedType genericThrowingProvider
+ = (ParameterizedType) interfaceType.getGenericInterfaces()[0];
+ if (interfaceType.getTypeParameters().length == 1) {
+ checkArgument(interfaceType.getTypeParameters().length == 1, errorMessage,
+ interfaceType.getName());
+ String returnTypeName = interfaceType.getTypeParameters()[0].getName();
+ Type returnType = genericThrowingProvider.getActualTypeArguments()[0];
+ checkArgument(returnType instanceof TypeVariable, errorMessage, interfaceType.getName());
+ checkArgument(returnTypeName.equals(((TypeVariable) returnType).getName()),
+ errorMessage, interfaceType.getName());
+ } else {
+ checkArgument(interfaceType.getTypeParameters().length == 0,
+ errorMessage, interfaceType.getName());
+ checkArgument(genericThrowingProvider.getActualTypeArguments()[0].equals(valueType),
+ errorMessage, interfaceType.getName());
+ }
+
+ Type exceptionType = genericThrowingProvider.getActualTypeArguments()[1];
+ checkArgument(exceptionType instanceof Class, errorMessage, interfaceType.getName());
+
+ if (interfaceType.getDeclaredMethods().length == 1) {
+ Method method = interfaceType.getDeclaredMethods()[0];
+ checkArgument(method.getName().equals("get"), errorMessage, interfaceType.getName());
+ checkArgument(method.getParameterTypes().length == 0,
+ errorMessage, interfaceType.getName());
+ } else {
+ checkArgument(interfaceType.getDeclaredMethods().length == 0,
+ errorMessage, interfaceType.getName());
+ }
+ }
+
+ private void checkArgument(boolean condition,
+ String messageFormat, Object... args) {
+ if (!condition) {
+ throw new IllegalArgumentException(String.format(messageFormat, args));
+ }
+ }
+
+ @SuppressWarnings({"unchecked"})
+ private Key<P> createKey() {
+ TypeLiteral<P> typeLiteral;
+ if (interfaceType.getTypeParameters().length == 1) {
+ typeLiteral = (TypeLiteral<P>) TypeLiteral.get(new ParameterizedType() {
+
+ public Type[] getActualTypeArguments() {
+ return new Type[]{valueType};
+ }
+
+ public Type getRawType() {
+ return interfaceType;
+ }
+
+ public Type getOwnerType() {
+ throw new UnsupportedOperationException();
+ }
+ });
+ } else {
+ typeLiteral = TypeLiteral.get(interfaceType);
+ }
+
+ if (annotation != null) {
+ return Key.get(typeLiteral, annotation);
+
+ } else if (annotationType != null) {
+ return Key.get(typeLiteral, annotationType);
+
+ } else {
+ return Key.get(typeLiteral);
+ }
+ }
+ }
+
+ /**
+ * Returns an annotation instance that is not equal to any other annotation
+ * instances, for use in creating distinct {@link Key}s.
+ */
+ private static Annotation uniqueAnnotation() {
+ return new Annotation() {
+ public Class<? extends Annotation> annotationType() {
+ return Internal.class;
+ }
+ };
+ }
+ @Retention(RUNTIME) @BindingAnnotation
+ private @interface Internal { }
+
+ /**
+ * Represents the returned value from a call to {@link
+ * ThrowingProvider#get()}. This is the value that will be scoped by Guice.
+ */
+ private static class Result {
+ private final Object value;
+ private final Exception exception;
+
+ private Result(Object value, Exception exception) {
+ this.value = value;
+ this.exception = exception;
+ }
+
+ public static Result forValue(Object value) {
+ return new Result(value, null);
+ }
+
+ public static Result forException(Exception e) {
+ return new Result(null, e);
+ }
+
+ public Object getOrThrow() throws Exception {
+ if (exception != null) {
+ throw exception;
+ } else {
+ return value;
+ }
+ }
+ }
+}
diff --git a/extensions/throwingproviders/test/com/google/inject/throwingproviders/TestScope.java b/extensions/throwingproviders/test/com/google/inject/throwingproviders/TestScope.java
new file mode 100644
index 0000000..1e435c3
--- /dev/null
+++ b/extensions/throwingproviders/test/com/google/inject/throwingproviders/TestScope.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright (C) 2007 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.throwingproviders;
+
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.Scope;
+import com.google.inject.ScopeAnnotation;
+import java.lang.annotation.Retention;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A simple scope that can be explicitly reset.
+ *
+ * @author jmourits@google.com (Jerome Mourits)
+ */
+class TestScope implements Scope {
+
+ @Retention(RUNTIME) @ScopeAnnotation
+ public @interface Scoped { }
+
+ private Map<Key, Object> inScopeObjectsMap = new HashMap<Key, Object>();
+
+ public <T> Provider<T> scope(
+ final Key<T> key, final Provider<T> provider) {
+ return new Provider<T>() {
+ @SuppressWarnings({"unchecked"})
+ public T get() {
+ T t = (T) inScopeObjectsMap.get(key);
+ if (t == null) {
+ t = provider.get();
+ inScopeObjectsMap.put(key, t);
+ }
+ return t;
+ }
+ };
+ }
+
+ public void beginNewScope() {
+ inScopeObjectsMap = new HashMap<Key, Object>();
+ }
+}
diff --git a/extensions/throwingproviders/test/com/google/inject/throwingproviders/ThrowingProviderBinderTest.java b/extensions/throwingproviders/test/com/google/inject/throwingproviders/ThrowingProviderBinderTest.java
new file mode 100644
index 0000000..f70bcc8
--- /dev/null
+++ b/extensions/throwingproviders/test/com/google/inject/throwingproviders/ThrowingProviderBinderTest.java
@@ -0,0 +1,276 @@
+/**
+ * Copyright (C) 2007 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.throwingproviders;
+
+import com.google.inject.AbstractModule;
+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 java.rmi.RemoteException;
+import java.util.Arrays;
+import java.util.List;
+import junit.framework.TestCase;
+
+/**
+ * @author jmourits@google.com (Jerome Mourits)
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public class ThrowingProviderBinderTest extends TestCase {
+
+ private final TypeLiteral<RemoteProvider<String>> remoteProviderOfString
+ = new TypeLiteral<RemoteProvider<String>>() { };
+ private final MockRemoteProvider<String> mockRemoteProvider = new MockRemoteProvider<String>();
+ private final TestScope testScope = new TestScope();
+ private Injector injector = Guice.createInjector(new AbstractModule() {
+ protected void configure() {
+ ThrowingProviderBinder.create(binder())
+ .bind(RemoteProvider.class, String.class)
+ .to(mockRemoteProvider)
+ .in(testScope);
+ }
+ });
+
+ public void testExceptionsThrown() {
+ RemoteProvider<String> remoteProvider =
+ injector.getInstance(Key.get(remoteProviderOfString));
+
+ mockRemoteProvider.throwOnNextGet("kaboom!");
+ try {
+ remoteProvider.get();
+ fail();
+ } catch (RemoteException expected) {
+ assertEquals("kaboom!", expected.getMessage());
+ }
+ }
+
+ public void testValuesScoped() throws RemoteException {
+ RemoteProvider<String> remoteProvider =
+ injector.getInstance(Key.get(remoteProviderOfString));
+
+ mockRemoteProvider.setNextToReturn("A");
+ assertEquals("A", remoteProvider.get());
+
+ mockRemoteProvider.setNextToReturn("B");
+ assertEquals("A", remoteProvider.get());
+
+ testScope.beginNewScope();
+ assertEquals("B", remoteProvider.get());
+ }
+
+ public void testExceptionsScoped() {
+ RemoteProvider<String> remoteProvider =
+ injector.getInstance(Key.get(remoteProviderOfString));
+
+ mockRemoteProvider.throwOnNextGet("A");
+ try {
+ remoteProvider.get();
+ fail();
+ } catch (RemoteException expected) {
+ assertEquals("A", expected.getMessage());
+ }
+
+ mockRemoteProvider.throwOnNextGet("B");
+ try {
+ remoteProvider.get();
+ fail();
+ } catch (RemoteException expected) {
+ assertEquals("A", expected.getMessage());
+ }
+ }
+
+ public void testAnnotations() throws RemoteException {
+ final MockRemoteProvider<String> mockRemoteProviderA = new MockRemoteProvider<String>();
+ mockRemoteProviderA.setNextToReturn("A");
+ final MockRemoteProvider<String> mockRemoteProviderB = new MockRemoteProvider<String>();
+ mockRemoteProviderB.setNextToReturn("B");
+
+ injector = Guice.createInjector(new AbstractModule() {
+ protected void configure() {
+ ThrowingProviderBinder.create(binder())
+ .bind(RemoteProvider.class, String.class)
+ .annotatedWith(Names.named("a"))
+ .to(mockRemoteProviderA);
+
+ ThrowingProviderBinder.create(binder())
+ .bind(RemoteProvider.class, String.class)
+ .to(mockRemoteProviderB);
+ }
+ });
+
+ assertEquals("A",
+ injector.getInstance(Key.get(remoteProviderOfString, Names.named("a"))).get());
+
+ assertEquals("B",
+ injector.getInstance(Key.get(remoteProviderOfString)).get());
+
+ }
+
+ public void testUndeclaredExceptions() throws RemoteException {
+ RemoteProvider<String> remoteProvider =
+ injector.getInstance(Key.get(remoteProviderOfString));
+
+ mockRemoteProvider.throwOnNextGet(new IndexOutOfBoundsException("A"));
+ try {
+ remoteProvider.get();
+ fail();
+ } catch (RuntimeException e) {
+ assertEquals("A", e.getMessage());
+ }
+
+ // undeclared exceptions shouldn't be scoped
+ mockRemoteProvider.throwOnNextGet(new IndexOutOfBoundsException("B"));
+ try {
+ remoteProvider.get();
+ fail();
+ } catch (RuntimeException e) {
+ assertEquals("B", e.getMessage());
+ }
+ }
+
+ public void testThrowingProviderSubclassing() throws RemoteException {
+ final SubMockRemoteProvider aProvider = new SubMockRemoteProvider();
+ aProvider.setNextToReturn("A");
+
+ injector = Guice.createInjector(new AbstractModule() {
+ protected void configure() {
+ ThrowingProviderBinder.create(binder())
+ .bind(RemoteProvider.class, String.class)
+ .to(aProvider);
+ }
+ });
+
+ assertEquals("A",
+ injector.getInstance(Key.get(remoteProviderOfString)).get());
+ }
+
+ static class SubMockRemoteProvider extends MockRemoteProvider<String> { }
+
+ public void testBindingToNonInterfaceType() throws RemoteException {
+ try {
+ injector = Guice.createInjector(new AbstractModule() {
+ protected void configure() {
+ ThrowingProviderBinder.create(binder())
+ .bind(MockRemoteProvider.class, String.class)
+ .to(mockRemoteProvider);
+ }
+ });
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().contains("is not a compliant interface"));
+ }
+ }
+
+ public void testBindingToSubSubInterface() throws RemoteException {
+ try {
+ injector = Guice.createInjector(new AbstractModule() {
+ protected void configure() {
+ ThrowingProviderBinder.create(binder())
+ .bind(SubRemoteProvider.class, String.class);
+ }
+ });
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().contains("is not a compliant interface"));
+ }
+ }
+
+ interface SubRemoteProvider extends RemoteProvider<String> { }
+
+ public void testBindingToInterfaceWithExtraMethod() throws RemoteException {
+ try {
+ injector = Guice.createInjector(new AbstractModule() {
+ protected void configure() {
+ ThrowingProviderBinder.create(binder())
+ .bind(RemoteProviderWithExtraMethod.class, String.class);
+ }
+ });
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().contains("is not a compliant interface"));
+ }
+ }
+
+ interface RemoteProviderWithExtraMethod<T> extends ThrowingProvider<T, RemoteException> {
+ T get(T defaultValue) throws RemoteException;
+ }
+
+ interface RemoteProvider<T> extends ThrowingProvider<T, RemoteException> { }
+
+ static class MockRemoteProvider<T> implements RemoteProvider<T> {
+ Exception nextToThrow;
+ T nextToReturn;
+
+ public void throwOnNextGet(String message) {
+ throwOnNextGet(new RemoteException(message));
+ }
+
+ public void throwOnNextGet(Exception nextToThrow) {
+ this.nextToThrow = nextToThrow;
+ }
+
+ public void setNextToReturn(T nextToReturn) {
+ this.nextToReturn = nextToReturn;
+ }
+
+ public T get() throws RemoteException {
+ if (nextToThrow instanceof RemoteException) {
+ throw (RemoteException) nextToThrow;
+ } else if (nextToThrow instanceof RuntimeException) {
+ throw (RuntimeException) nextToThrow;
+ } else if (nextToThrow == null) {
+ return nextToReturn;
+ } else {
+ throw new AssertionError("nextToThrow must be a runtime or remote exception");
+ }
+ }
+ }
+
+ public void testBindingToInterfaceWithBoundValueType() throws RemoteException {
+ injector = Guice.createInjector(new AbstractModule() {
+ protected void configure() {
+ ThrowingProviderBinder.create(binder())
+ .bind(StringRemoteProvider.class, String.class)
+ .to(new StringRemoteProvider() {
+ public String get() throws RemoteException {
+ return "A";
+ }
+ });
+ }
+ });
+
+ assertEquals("A", injector.getInstance(StringRemoteProvider.class).get());
+ }
+
+ interface StringRemoteProvider extends ThrowingProvider<String, RemoteException> { }
+
+ public void testBindingToInterfaceWithGeneric() throws RemoteException {
+ injector = Guice.createInjector(new AbstractModule() {
+ protected void configure() {
+ ThrowingProviderBinder.create(binder())
+ .bind(RemoteProvider.class, new TypeLiteral<List<String>>() { }.getType())
+ .to(new RemoteProvider<List<String>>() {
+ public List<String> get() throws RemoteException {
+ return Arrays.asList("A", "B");
+ }
+ });
+ }
+ });
+
+ Key<RemoteProvider<List<String>>> key
+ = Key.get(new TypeLiteral<RemoteProvider<List<String>>>() { });
+ assertEquals(Arrays.asList("A", "B"), injector.getInstance(key).get());
+ }
+}
diff --git a/extensions/throwingproviders/throwingproviders.iml b/extensions/throwingproviders/throwingproviders.iml
new file mode 100644
index 0000000..336fd39
--- /dev/null
+++ b/extensions/throwingproviders/throwingproviders.iml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module version="4" relativePaths="true" type="JAVA_MODULE">
+ <component name="ModuleRootManager" />
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
+ <exclude-output />
+ <content url="file://$MODULE_DIR$">
+ <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
+ </content>
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ <orderEntry type="module" module-name="guice" />
+ <orderEntry type="module-library">
+ <library>
+ <CLASSES>
+ <root url="jar://$MODULE_DIR$/../../lib/build/junit.jar!/" />
+ </CLASSES>
+ <JAVADOC />
+ <SOURCES />
+ </library>
+ </orderEntry>
+ <orderEntryProperties />
+ </component>
+</module>
+
diff --git a/guice.iml b/guice.iml
index f5b8cbc..44ee798 100644
--- a/guice.iml
+++ b/guice.iml
@@ -9,7 +9,6 @@
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/classes" />
<excludeFolder url="file://$MODULE_DIR$/javadoc" />
- <excludeFolder url="file://$MODULE_DIR$/lib" />
<excludeFolder url="file://$MODULE_DIR$/struts2" />
</content>
<orderEntry type="inheritedJdk" />
@@ -86,6 +85,33 @@
<SOURCES />
</library>
</orderEntry>
+ <orderEntry type="module-library">
+ <library>
+ <CLASSES>
+ <root url="jar://$MODULE_DIR$/lib/build/asm-3.0.jar!/" />
+ </CLASSES>
+ <JAVADOC />
+ <SOURCES />
+ </library>
+ </orderEntry>
+ <orderEntry type="module-library">
+ <library>
+ <CLASSES>
+ <root url="jar://$MODULE_DIR$/lib/build/cglib-nodep-2.2_beta1.jar!/" />
+ </CLASSES>
+ <JAVADOC />
+ <SOURCES />
+ </library>
+ </orderEntry>
+ <orderEntry type="module-library">
+ <library>
+ <CLASSES>
+ <root url="jar://$MODULE_DIR$/lib/build/cglib-src-2.2_beta1.jar!/" />
+ </CLASSES>
+ <JAVADOC />
+ <SOURCES />
+ </library>
+ </orderEntry>
<orderEntryProperties />
</component>
</module>