com.google.inject.assistedinject.FactoryProvider Maven / Gradle / Ivy
/*
* 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 com.google.inject.internal.Annotations.getKey;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.inject.ConfigurationException;
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.internal.Errors;
import com.google.inject.internal.ErrorsException;
import com.google.inject.spi.Dependency;
import com.google.inject.spi.HasDependencies;
import com.google.inject.spi.Message;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Obsolete. Prefer {@link FactoryModuleBuilder} for its more concise API and
* additional capability.
*
* Provides a factory that combines the caller's arguments with injector-supplied values to
* construct objects.
*
*
Defining a factory
*
* Create an interface whose methods return the constructed type, or any of its supertypes. The
* method's parameters are the arguments required to build the constructed type.
*
* public interface PaymentFactory {
* Payment create(Date startDate, Money amount);
* }
*
* You can name your factory methods whatever you like, such as create, createPayment
* or newPayment.
*
* Creating a type that accepts factory parameters
*
* {@code constructedType} is a concrete class with an {@literal @}{@link Inject}-annotated
* constructor. In addition to injector-supplied parameters, the constructor should have parameters
* that match each of the factory method's parameters. Each factory-supplied parameter requires an
* {@literal @}{@link Assisted} annotation. This serves to document that the parameter is not bound
* by your application's modules.
*
* public class RealPayment implements Payment {
* {@literal @}Inject
* public RealPayment(
* CreditService creditService,
* AuthService authService,
* {@literal @}Assisted Date startDate,
* {@literal @}Assisted Money amount) {
* ...
* }
* }
*
* Any parameter that permits a null value should also be annotated {@code @Nullable}.
*
* Configuring factories
*
* In your {@link com.google.inject.Module module}, bind the factory interface to the returned
* factory:
*
* bind(PaymentFactory.class).toProvider(
* FactoryProvider.newFactory(PaymentFactory.class, RealPayment.class));
*
* As a side-effect of this binding, Guice will inject the factory to initialize it for use. The
* factory cannot be used until the injector has been initialized.
*
* Using the factory
*
* Inject your factory into your application classes. When you use the factory, your arguments will
* be combined with values from the injector to construct an instance.
*
* public class PaymentAction {
* {@literal @}Inject private PaymentFactory paymentFactory;
*
* public void doPayment(Money amount) {
* Payment payment = paymentFactory.create(new Date(), amount);
* payment.apply();
* }
* }
*
* Making parameter types distinct
*
* The types of the factory method's parameters must be distinct. To use multiple parameters of the
* same type, use a named {@literal @}{@link Assisted} annotation to disambiguate the parameters.
* The names must be applied to the factory method's parameters:
*
* public interface PaymentFactory {
* Payment create(
* {@literal @}Assisted("startDate") Date startDate,
* {@literal @}Assisted("dueDate") Date dueDate,
* Money amount);
* }
*
* ...and to the concrete type's constructor parameters:
*
* public class RealPayment implements Payment {
* {@literal @}Inject
* public RealPayment(
* CreditService creditService,
* AuthService authService,
* {@literal @}Assisted("startDate") Date startDate,
* {@literal @}Assisted("dueDate") Date dueDate,
* {@literal @}Assisted Money amount) {
* ...
* }
* }
*
* Values are created by Guice
*
* Returned factories use child injectors to create values. The values are eligible for method
* interception. In addition, {@literal @}{@literal Inject} members will be injected before they are
* returned.
*
* Backwards compatibility using {@literal @}AssistedInject
*
* Instead of the {@literal @}Inject annotation, you may annotate the constructed classes with
* {@literal @}{@link AssistedInject}. This triggers a limited backwards-compatability mode.
*
* Instead of matching factory method arguments to constructor parameters using their names, the
* parameters are matched by their order. The first factory method argument is used
* for the first {@literal @}Assisted constructor parameter, etc.. Annotation names have no effect.
*
*
Returned values are not created by Guice. These types are not eligible for
* method interception. They do receive post-construction member injection.
*
* @param The factory interface
* @author [email protected] (Jerome Mourits)
* @author [email protected] (Jesse Wilson)
* @author [email protected] (Daniel Martin)
* @deprecated use {@link FactoryModuleBuilder} instead.
*/
@Deprecated
public class FactoryProvider implements Provider, HasDependencies {
/*
* This class implements the old @AssistedInject implementation that manually matches constructors
* to factory methods. The new child injector implementation lives in FactoryProvider2.
*/
private Injector injector;
private final TypeLiteral factoryType;
private final TypeLiteral> implementationType;
private final Map> factoryMethodToConstructor;
public static Provider newFactory(Class factoryType, Class> implementationType) {
return newFactory(TypeLiteral.get(factoryType), TypeLiteral.get(implementationType));
}
public static Provider newFactory(
TypeLiteral factoryType, TypeLiteral> implementationType) {
Map> factoryMethodToConstructor =
createMethodMapping(factoryType, implementationType);
if (!factoryMethodToConstructor.isEmpty()) {
return new FactoryProvider(factoryType, implementationType, factoryMethodToConstructor);
} else {
BindingCollector collector = new BindingCollector();
// Preserving backwards-compatibility: Map all return types in a factory
// interface to the passed implementation type.
Errors errors = new Errors();
Key> implementationKey = Key.get(implementationType);
try {
for (Method method : factoryType.getRawType().getMethods()) {
Key> returnType =
getKey(factoryType.getReturnType(method), method, method.getAnnotations(), errors);
if (!implementationKey.equals(returnType)) {
collector.addBinding(returnType, implementationType);
}
}
} catch (ErrorsException e) {
throw new ConfigurationException(e.getErrors().getMessages());
}
return new FactoryProvider2(Key.get(factoryType), collector);
}
}
private FactoryProvider(
TypeLiteral factoryType,
TypeLiteral> implementationType,
Map> factoryMethodToConstructor) {
this.factoryType = factoryType;
this.implementationType = implementationType;
this.factoryMethodToConstructor = factoryMethodToConstructor;
checkDeclaredExceptionsMatch();
}
@Inject
void setInjectorAndCheckUnboundParametersAreInjectable(Injector injector) {
this.injector = injector;
for (AssistedConstructor> c : factoryMethodToConstructor.values()) {
for (Parameter p : c.getAllParameters()) {
if (!p.isProvidedByFactory() && !paramCanBeInjected(p, injector)) {
// this is lame - we're not using the proper mechanism to add an
// error to the injector. Throughout this class we throw exceptions
// to add errors, which isn't really the best way in Guice
throw newConfigurationException(
"Parameter of type '%s' is not injectable or annotated "
+ "with @Assisted for Constructor '%s'",
p, c);
}
}
}
}
private void checkDeclaredExceptionsMatch() {
for (Map.Entry> entry : factoryMethodToConstructor.entrySet()) {
for (Class> constructorException : entry.getValue().getDeclaredExceptions()) {
if (!isConstructorExceptionCompatibleWithFactoryExeception(
constructorException, entry.getKey().getExceptionTypes())) {
throw newConfigurationException(
"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;
}
private boolean paramCanBeInjected(Parameter parameter, Injector injector) {
return parameter.isBound(injector);
}
private static Map> createMethodMapping(
TypeLiteral> factoryType, TypeLiteral> implementationType) {
List> constructors = Lists.newArrayList();
for (Constructor> constructor : implementationType.getRawType().getDeclaredConstructors()) {
if (constructor.isAnnotationPresent(AssistedInject.class)) {
AssistedConstructor> assistedConstructor =
AssistedConstructor.create(
constructor, implementationType.getParameterTypes(constructor));
constructors.add(assistedConstructor);
}
}
if (constructors.isEmpty()) {
return ImmutableMap.of();
}
Method[] factoryMethods = factoryType.getRawType().getMethods();
if (constructors.size() != factoryMethods.length) {
throw newConfigurationException(
"Constructor mismatch: %s has %s @AssistedInject "
+ "constructors, factory %s has %s creation methods",
implementationType, constructors.size(), factoryType, factoryMethods.length);
}
Map> paramsToConstructor = Maps.newHashMap();
for (AssistedConstructor> c : constructors) {
if (paramsToConstructor.containsKey(c.getAssistedParameters())) {
throw new RuntimeException("Duplicate constructor, " + c);
}
paramsToConstructor.put(c.getAssistedParameters(), c);
}
Map> result = Maps.newHashMap();
for (Method method : factoryMethods) {
if (!method.getReturnType().isAssignableFrom(implementationType.getRawType())) {
throw newConfigurationException(
"Return type of method %s is not assignable from %s", method, implementationType);
}
List parameterTypes = Lists.newArrayList();
for (TypeLiteral> parameterType : factoryType.getParameterTypes(method)) {
parameterTypes.add(parameterType.getType());
}
ParameterListKey methodParams = new ParameterListKey(parameterTypes);
if (!paramsToConstructor.containsKey(methodParams)) {
throw newConfigurationException(
"%s has no @AssistInject constructor that takes the "
+ "@Assisted parameters %s in that order. @AssistInject constructors are %s",
implementationType, methodParams, paramsToConstructor.values());
}
method.getParameterAnnotations();
for (Annotation[] parameterAnnotations : method.getParameterAnnotations()) {
for (Annotation parameterAnnotation : parameterAnnotations) {
if (parameterAnnotation.annotationType() == Assisted.class) {
throw newConfigurationException(
"Factory method %s has an @Assisted parameter, which "
+ "is incompatible with the deprecated @AssistedInject annotation. Please replace "
+ "@AssistedInject with @Inject on the %s constructor.",
method, implementationType);
}
}
}
AssistedConstructor> matchingConstructor = paramsToConstructor.remove(methodParams);
result.put(method, matchingConstructor);
}
return result;
}
@Override
public Set> getDependencies() {
List> dependencies = Lists.newArrayList();
for (AssistedConstructor> constructor : factoryMethodToConstructor.values()) {
for (Parameter parameter : constructor.getAllParameters()) {
if (!parameter.isProvidedByFactory()) {
dependencies.add(Dependency.get(parameter.getPrimaryBindingKey()));
}
}
}
return ImmutableSet.copyOf(dependencies);
}
@Override
public F get() {
InvocationHandler invocationHandler =
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] creationArgs)
throws Throwable {
// pass methods from Object.class to the proxy
if (method.getDeclaringClass().equals(Object.class)) {
if ("equals".equals(method.getName())) {
return proxy == creationArgs[0];
} else if ("hashCode".equals(method.getName())) {
return System.identityHashCode(proxy);
} else {
return method.invoke(this, creationArgs);
}
}
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;
}
};
@SuppressWarnings("unchecked") // we imprecisely treat the class literal of T as a Class
Class factoryRawType = (Class) (Class>) factoryType.getRawType();
return factoryRawType.cast(
Proxy.newProxyInstance(
factoryRawType.getClassLoader(),
new Class[] {factoryRawType},
invocationHandler));
}
@Override
public int hashCode() {
return Objects.hashCode(factoryType, implementationType);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof FactoryProvider)) {
return false;
}
FactoryProvider> other = (FactoryProvider>) obj;
return factoryType.equals(other.factoryType)
&& implementationType.equals(other.implementationType);
}
private static ConfigurationException newConfigurationException(String format, Object... args) {
return new ConfigurationException(ImmutableSet.of(new Message(Errors.format(format, args))));
}
}