com.google.inject.throwingproviders.ThrowingProviderBinder 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.throwingproviders;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
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.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.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;
/**
* Builds a binding for a {@link CheckedProvider}.
*
*
You can use a fluent API and custom providers:
*
ThrowingProviderBinder.create(binder())
* .bind(RemoteProvider.class, Customer.class)
* .to(RemoteCustomerProvider.class)
* .in(RequestScope.class);
*
* or, you can use throwing provider methods:
* class MyModule extends AbstractModule {
* configure() {
* ThrowingProviderBinder.install(this, binder());
* }
*
* {@literal @}CheckedProvides(RemoteProvider.class)
* {@literal @}RequestScope
* Customer provideCustomer(FlakyCustomerCreator creator) throws RemoteException {
* return creator.getCustomerOrThrow();
* }
* }
*
* You also can declare that a CheckedProvider construct
* a particular class whose constructor throws an exception:
* ThrowingProviderBinder.create(binder())
* .bind(RemoteProvider.class, Customer.class)
* .providing(CustomerImpl.class)
* .in(RequestScope.class);
*
*
* @author [email protected] (Jerome Mourits)
* @author [email protected] (Jesse Wilson)
* @author [email protected] (Sam Berlin)
*/
public class ThrowingProviderBinder {
private static final TypeLiteral> CHECKED_PROVIDER_TYPE
= new TypeLiteral>() { };
private static final TypeLiteral> CHECKED_PROVIDER_METHOD_TYPE
= new TypeLiteral>() { };
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 SecondaryBinder
bind(Class
interfaceType, Type clazz) {
return new SecondaryBinder
(interfaceType, clazz);
}
/**
* @since 4.0
*/
public
SecondaryBinder
bind(Class
interfaceType, Class clazz) {
return new SecondaryBinder(interfaceType, clazz);
}
/**
* @since 4.0
*/
public
SecondaryBinder
bind(Class
interfaceType, TypeLiteral typeLiteral) {
return new SecondaryBinder(interfaceType, typeLiteral.getType());
}
public class SecondaryBinder
{
private final Class
interfaceType;
private final Type valueType;
private final List> exceptionTypes;
private final boolean valid;
private Class extends Annotation> annotationType;
private Annotation annotation;
private Key interfaceKey;
private boolean scopeExceptions = true;
public SecondaryBinder(Class
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> getExceptionTypes() {
return exceptionTypes;
}
Key getKey() {
return interfaceKey;
}
public SecondaryBinder
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
annotatedWith(Annotation annotation) {
if (!(this.annotationType == null && this.annotation == null)) {
throw new IllegalStateException("Cannot set annotation twice");
}
this.annotation = annotation;
return this;
}
/**
* Determines if exceptions should be scoped. By default exceptions are scoped.
*
* @param scopeExceptions whether exceptions should be scoped.
* @since 4.0
*/
public SecondaryBinder
scopeExceptions(boolean scopeExceptions) {
this.scopeExceptions = scopeExceptions;
return this;
}
public ScopedBindingBuilder to(P target) {
Key
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));
}
/** @since 4.0 */
public ScopedBindingBuilder providing(Class extends T> cxtorClass) {
return providing(TypeLiteral.get(cxtorClass));
}
/** @since 4.0 */
@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 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) typeKey);
} else {
// never used, but need it assigned.
typeProvider = null;
typeKey = null;
}
// Create a CheckedProvider that calls our cxtor
CheckedProvider checkedProvider = new CheckedProviderWithDependencies() {
@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> getDependencies() {
return ImmutableSet.>of(Dependency.get(typeKey));
}
};
Key> targetKey = Key.get(CHECKED_PROVIDER_TYPE,
UniqueAnnotations.create());
binder.bind(targetKey).toInstance(checkedProvider);
return toInternal(targetKey);
}
ScopedBindingBuilder toProviderMethod(CheckedProviderMethod> target) {
Key> targetKey =
Key.get(CHECKED_PROVIDER_METHOD_TYPE, UniqueAnnotations.create());
binder.bind(targetKey).toInstance(target);
return toInternal(targetKey);
}
@SuppressWarnings("unchecked") // P only extends the raw type of CheckedProvider
public ScopedBindingBuilder to(Key extends P> targetKey) {
checkNotNull(targetKey, "targetKey");
return toInternal((Key extends CheckedProvider>>)targetKey);
}
private ScopedBindingBuilder toInternal(final Key extends CheckedProvider>> targetKey) {
final Key resultKey = Key.get(Result.class, UniqueAnnotations.create());
// Note that this provider will behave like the final provider Guice creates.
// It will especially do scoping if the user adds that.
final Provider 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() {
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);
}
if (scopeExceptions) {
return resultProvider.get().getOrThrow();
} else {
Result result;
try {
result = resultProvider.get();
} catch (ProvisionException pe) {
Throwable cause = pe.getCause();
if (cause instanceof ResultException) {
throw ((ResultException)cause).getCause();
} else {
throw pe;
}
}
return result.getOrThrow();
}
}
}));
@Override
public P get() {
return instance;
}
@Override
public Set> getDependencies() {
return ImmutableSet.>of(Dependency.get(resultKey));
}
});
}
// The provider is unscoped, but the user may apply a scope to it through the
// ScopedBindingBuilder this returns.
return binder.bind(resultKey).toProvider(
createResultProvider(targetKey, targetProvider));
}
private ProviderWithDependencies createResultProvider(
final Key extends CheckedProvider>> targetKey,
final Provider extends CheckedProvider>> targetProvider) {
return new ProviderWithDependencies() {
@Override
public Result get() {
try {
return Result.forValue(targetProvider.get().get());
} catch (Exception e) {
for (Class extends Throwable> exceptionType : exceptionTypes) {
if (exceptionType.isInstance(e)) {
if (scopeExceptions) {
return Result.forException(e);
} else {
throw new ResultException(e);
}
}
}
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
} else {
// this should never happen
throw new RuntimeException(e);
}
}
}
@Override
public Set> getDependencies() {
return ImmutableSet.>of(Dependency.get(targetKey));
}
};
}
/**
* Returns the exception type declared to be thrown by the get method of
* {@code interfaceType}.
*/
private List> getExceptionType(Class interfaceType) {
try {
Method getMethod = interfaceType.getMethod("get");
List> exceptionLiterals =
TypeLiteral.get(interfaceType).getExceptionTypes(getMethod);
List> 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;
}
}
// Skip synthetic/bridge methods because java8 generates
// a default method on the interface w/ the superinterface type that
// just delegates directly to the overridden method.
List declaredMethods = FluentIterable
.from(Arrays.asList(interfaceType.getDeclaredMethods()))
.filter(NotSyntheticOrBridgePredicate.INSTANCE)
.toList();
if (declaredMethods.size() == 1) {
Method method = declaredMethods.get(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(declaredMethods.isEmpty(),
"%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 createKey() {
TypeLiteral
typeLiteral;
if (interfaceType.getTypeParameters().length == 1) {
ParameterizedType type = Types.newParameterizedTypeWithOwner(
interfaceType.getEnclosingClass(), interfaceType, valueType);
typeLiteral = (TypeLiteral
) 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 static final long serialVersionUID = 0L;
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;
}
}
}
/**
* RuntimeException class to wrap exceptions from the checked provider.
* The regular guice provider can throw it and the checked provider proxy extracts
* the underlying exception and rethrows it.
*/
private static class ResultException extends RuntimeException {
ResultException(Exception cause) {
super(cause);
}
}
private static class NotSyntheticOrBridgePredicate implements Predicate {
static NotSyntheticOrBridgePredicate INSTANCE = new NotSyntheticOrBridgePredicate();
@Override public boolean apply(Method input) {
return !input.isBridge() && !input.isSynthetic();
}
}
}