blob: 01cbee5729f57163697fe184452b8513f1b8b903 [file] [log] [blame]
/**
* 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 static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.inject.Binder;
import com.google.inject.ConfigurationException;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import com.google.inject.Scopes;
import com.google.inject.TypeLiteral;
import com.google.inject.binder.ScopedBindingBuilder;
import com.google.inject.internal.UniqueAnnotations;
import com.google.inject.spi.Dependency;
import com.google.inject.spi.InjectionPoint;
import com.google.inject.spi.Message;
import com.google.inject.spi.ProviderWithDependencies;
import com.google.inject.util.Types;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
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;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
/**
* <p>Builds a binding for a {@link CheckedProvider}.
*
* <p>You can use a fluent API and custom providers:
* <pre><code>ThrowingProviderBinder.create(binder())
* .bind(RemoteProvider.class, Customer.class)
* .to(RemoteCustomerProvider.class)
* .in(RequestScope.class);
* </code></pre>
* or, you can use throwing provider methods:
* <pre><code>class MyModule extends AbstractModule {
* configure() {
* ThrowingProviderBinder.install(this, binder());
* }
*
* {@literal @}CheckedProvides(RemoteProvider.class)
* {@literal @}RequestScope
* Customer provideCustomer(FlakyCustomerCreator creator) throws RemoteException {
* return creator.getCustomerOrThrow();
* }
* }
* </code></pre>
* You also can declare that a CheckedProvider construct
* a particular class whose constructor throws an exception:
* <pre><code>ThrowingProviderBinder.create(binder())
* .bind(RemoteProvider.class, Customer.class)
* .providing(CustomerImpl.class)
* .in(RequestScope.class);
* </code></pre>
*
* @author jmourits@google.com (Jerome Mourits)
* @author jessewilson@google.com (Jesse Wilson)
* @author sameb@google.com (Sam Berlin)
*/
public class ThrowingProviderBinder {
private final Binder binder;
private ThrowingProviderBinder(Binder binder) {
this.binder = binder;
}
public static ThrowingProviderBinder create(Binder binder) {
return new ThrowingProviderBinder(binder.skipSources(
ThrowingProviderBinder.class,
ThrowingProviderBinder.SecondaryBinder.class));
}
/**
* Returns a module that installs {@literal @}{@link CheckedProvides} methods.
*
* @since 3.0
*/
public static Module forModule(Module module) {
return CheckedProviderMethodsModule.forModule(module);
}
/**
* @deprecated Use {@link #bind(Class, Class)} or {@link #bind(Class, TypeLiteral)} instead.
*/
@Deprecated
public <P extends CheckedProvider> SecondaryBinder<P, ?>
bind(Class<P> interfaceType, Type clazz) {
return new SecondaryBinder<P, Object>(interfaceType, clazz);
}
public <P extends CheckedProvider, T> SecondaryBinder<P, T>
bind(Class<P> interfaceType, Class<T> clazz) {
return new SecondaryBinder<P, T>(interfaceType, clazz);
}
public <P extends CheckedProvider, T> SecondaryBinder<P, T>
bind(Class<P> interfaceType, TypeLiteral<T> typeLiteral) {
return new SecondaryBinder<P, T>(interfaceType, typeLiteral.getType());
}
public class SecondaryBinder<P extends CheckedProvider, T> {
private final Class<P> interfaceType;
private final Type valueType;
private final List<Class<? extends Throwable>> exceptionTypes;
private final boolean valid;
private Class<? extends Annotation> annotationType;
private Annotation annotation;
private Key<P> interfaceKey;
public SecondaryBinder(Class<P> interfaceType, Type valueType) {
this.interfaceType = checkNotNull(interfaceType, "interfaceType");
this.valueType = checkNotNull(valueType, "valueType");
if(checkInterface()) {
this.exceptionTypes = getExceptionType(interfaceType);
valid = true;
} else {
valid = false;
this.exceptionTypes = ImmutableList.of();
}
}
List<Class<? extends Throwable>> getExceptionTypes() {
return exceptionTypes;
}
Key<P> getKey() {
return interfaceKey;
}
public SecondaryBinder<P, T> annotatedWith(Class<? extends Annotation> annotationType) {
if (!(this.annotationType == null && this.annotation == null)) {
throw new IllegalStateException("Cannot set annotation twice");
}
this.annotationType = annotationType;
return this;
}
public SecondaryBinder<P, T> annotatedWith(Annotation annotation) {
if (!(this.annotationType == null && this.annotation == null)) {
throw new IllegalStateException("Cannot set annotation twice");
}
this.annotation = annotation;
return this;
}
public ScopedBindingBuilder to(P target) {
Key<P> targetKey = Key.get(interfaceType, UniqueAnnotations.create());
binder.bind(targetKey).toInstance(target);
return to(targetKey);
}
public ScopedBindingBuilder to(Class<? extends P> targetType) {
return to(Key.get(targetType));
}
public ScopedBindingBuilder providing(Class<? extends T> cxtorClass) {
return providing(TypeLiteral.get(cxtorClass));
}
@SuppressWarnings("unchecked") // safe because this is the cxtor of the literal
public ScopedBindingBuilder providing(TypeLiteral<? extends T> cxtorLiteral) {
// Find a constructor that has @ThrowingInject.
Constructor<? extends T> cxtor =
CheckedProvideUtils.findThrowingConstructor(cxtorLiteral, binder);
final Provider<T> typeProvider;
final Key<? extends T> typeKey;
// If we found an injection point, then bind the cxtor to a unique key
if (cxtor != null) {
// Validate the exceptions are consistent with the CheckedProvider interface.
CheckedProvideUtils.validateExceptions(
binder, cxtorLiteral.getExceptionTypes(cxtor), exceptionTypes, interfaceType);
typeKey = Key.get(cxtorLiteral, UniqueAnnotations.create());
binder.bind(typeKey).toConstructor((Constructor) cxtor).in(Scopes.NO_SCOPE);
typeProvider = binder.getProvider((Key<T>) typeKey);
} else {
// never used, but need it assigned.
typeProvider = null;
typeKey = null;
}
// Create a CheckedProvider that calls our cxtor
CheckedProvider<T> checkedProvider = new CheckedProviderWithDependencies<T>() {
@Override
public T get() throws Exception {
try {
return typeProvider.get();
} catch (ProvisionException pe) {
// Rethrow the provision cause as the actual exception
if (pe.getCause() instanceof Exception) {
throw (Exception) pe.getCause();
} else if (pe.getCause() instanceof Error) {
throw (Error) pe.getCause();
} else {
// If this failed because of multiple reasons (ie, more than
// one dependency failed due to scoping errors), then
// the ProvisionException won't have a cause, so we need
// to rethrow it as-is.
throw pe;
}
}
}
@Override
public Set<Dependency<?>> getDependencies() {
return ImmutableSet.<Dependency<?>>of(Dependency.get(typeKey));
}
};
Key<CheckedProvider> targetKey = Key.get(CheckedProvider.class, UniqueAnnotations.create());
binder.bind(targetKey).toInstance(checkedProvider);
return toInternal(targetKey);
}
ScopedBindingBuilder toProviderMethod(CheckedProviderMethod<?> target) {
Key<CheckedProviderMethod> targetKey =
Key.get(CheckedProviderMethod.class, UniqueAnnotations.create());
binder.bind(targetKey).toInstance(target);
return toInternal(targetKey);
}
public ScopedBindingBuilder to(Key<? extends P> targetKey) {
checkNotNull(targetKey, "targetKey");
return toInternal(targetKey);
}
private ScopedBindingBuilder toInternal(final Key<? extends CheckedProvider> targetKey) {
final Key<Result> resultKey = Key.get(Result.class, UniqueAnnotations.create());
final Provider<Result> resultProvider = binder.getProvider(resultKey);
final Provider<? extends CheckedProvider> targetProvider = binder.getProvider(targetKey);
interfaceKey = createKey();
// don't bother binding the proxy type if this is in an invalid state.
if(valid) {
binder.bind(interfaceKey).toProvider(new ProviderWithDependencies<P>() {
private final P instance = interfaceType.cast(Proxy.newProxyInstance(
interfaceType.getClassLoader(), new Class<?>[] { interfaceType },
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// Allow methods like .equals(..), .hashcode(..), .toString(..) to work.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
return resultProvider.get().getOrThrow();
}
}));
public P get() {
return instance;
}
public Set<Dependency<?>> getDependencies() {
return ImmutableSet.<Dependency<?>>of(Dependency.get(resultKey));
}
});
}
return binder.bind(resultKey).toProvider(new ProviderWithDependencies<Result>() {
public Result get() {
try {
return Result.forValue(targetProvider.get().get());
} catch (Exception e) {
for(Class<? extends Throwable> exceptionType : exceptionTypes) {
if (exceptionType.isInstance(e)) {
return Result.forException(e);
}
}
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
} else {
// this should never happen
throw new RuntimeException(e);
}
}
}
public Set<Dependency<?>> getDependencies() {
return ImmutableSet.<Dependency<?>>of(Dependency.get(targetKey));
}
});
}
/**
* Returns the exception type declared to be thrown by the get method of
* {@code interfaceType}.
*/
private List<Class<? extends Throwable>> getExceptionType(Class<P> interfaceType) {
try {
Method getMethod = interfaceType.getMethod("get");
List<TypeLiteral<?>> exceptionLiterals =
TypeLiteral.get(interfaceType).getExceptionTypes(getMethod);
List<Class<? extends Throwable>> results = Lists.newArrayList();
for (TypeLiteral<?> exLiteral : exceptionLiterals) {
results.add(exLiteral.getRawType().asSubclass(Throwable.class));
}
return results;
} catch (SecurityException e) {
throw new IllegalStateException("Not allowed to inspect exception types", e);
} catch (NoSuchMethodException e) {
throw new IllegalStateException("No 'get'method available", e);
}
}
private boolean checkInterface() {
if(!checkArgument(interfaceType.isInterface(),
"%s must be an interface", interfaceType.getName())) {
return false;
}
if(!checkArgument(interfaceType.getGenericInterfaces().length == 1,
"%s must extend CheckedProvider (and only CheckedProvider)",
interfaceType)) {
return false;
}
boolean tpMode = interfaceType.getInterfaces()[0] == ThrowingProvider.class;
if(!tpMode) {
if(!checkArgument(interfaceType.getInterfaces()[0] == CheckedProvider.class,
"%s must extend CheckedProvider (and only CheckedProvider)",
interfaceType)) {
return false;
}
}
// Ensure that T is parameterized and unconstrained.
ParameterizedType genericThrowingProvider
= (ParameterizedType) interfaceType.getGenericInterfaces()[0];
if (interfaceType.getTypeParameters().length == 1) {
String returnTypeName = interfaceType.getTypeParameters()[0].getName();
Type returnType = genericThrowingProvider.getActualTypeArguments()[0];
if(!checkArgument(returnType instanceof TypeVariable,
"%s does not properly extend CheckedProvider, the first type parameter of CheckedProvider (%s) is not a generic type",
interfaceType, returnType)) {
return false;
}
if(!checkArgument(returnTypeName.equals(((TypeVariable) returnType).getName()),
"The generic type (%s) of %s does not match the generic type of CheckedProvider (%s)",
returnTypeName, interfaceType, ((TypeVariable)returnType).getName())) {
return false;
}
} else {
if(!checkArgument(interfaceType.getTypeParameters().length == 0,
"%s has more than one generic type parameter: %s",
interfaceType, Arrays.asList(interfaceType.getTypeParameters()))) {
return false;
}
if(!checkArgument(genericThrowingProvider.getActualTypeArguments()[0].equals(valueType),
"%s expects the value type to be %s, but it was %s",
interfaceType, genericThrowingProvider.getActualTypeArguments()[0], valueType)) {
return false;
}
}
if(tpMode) { // only validate exception in ThrowingProvider mode.
Type exceptionType = genericThrowingProvider.getActualTypeArguments()[1];
if(!checkArgument(exceptionType instanceof Class,
"%s has the wrong Exception generic type (%s) when extending CheckedProvider",
interfaceType, exceptionType)) {
return false;
}
}
if (interfaceType.getDeclaredMethods().length == 1) {
Method method = interfaceType.getDeclaredMethods()[0];
if(!checkArgument(method.getName().equals("get"),
"%s may not declare any new methods, but declared %s",
interfaceType, method)) {
return false;
}
if(!checkArgument(method.getParameterTypes().length == 0,
"%s may not declare any new methods, but declared %s",
interfaceType, method.toGenericString())) {
return false;
}
} else {
if(!checkArgument(interfaceType.getDeclaredMethods().length == 0,
"%s may not declare any new methods, but declared %s",
interfaceType, Arrays.asList(interfaceType.getDeclaredMethods()))) {
return false;
}
}
return true;
}
private boolean checkArgument(boolean condition,
String messageFormat, Object... args) {
if (!condition) {
binder.addError(messageFormat, args);
return false;
} else {
return true;
}
}
@SuppressWarnings({"unchecked"})
private Key<P> createKey() {
TypeLiteral<P> typeLiteral;
if (interfaceType.getTypeParameters().length == 1) {
ParameterizedType type = Types.newParameterizedTypeWithOwner(
interfaceType.getEnclosingClass(), interfaceType, valueType);
typeLiteral = (TypeLiteral<P>) TypeLiteral.get(type);
} 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);
}
}
}
/**
* Represents the returned value from a call to {@link
* CheckedProvider#get()}. This is the value that will be scoped by Guice.
*/
static class Result implements Serializable {
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;
}
}
private static final long serialVersionUID = 0L;
}
}