blob: 04fe743843f93b08bdb2d6aba9fb9232101f1875 [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 com.google.inject.Binder;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.Provider;
import com.google.inject.TypeLiteral;
import com.google.inject.binder.ScopedBindingBuilder;
import static com.google.inject.internal.util.Preconditions.checkNotNull;
import com.google.inject.internal.UniqueAnnotations;
import com.google.inject.internal.util.ImmutableList;
import com.google.inject.internal.util.ImmutableSet;
import com.google.inject.internal.util.Lists;
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.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>
*
* @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.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 ThrowingProviderMethodsModule.forModule(module);
}
public <P extends CheckedProvider> SecondaryBinder<P>
bind(final Class<P> interfaceType, final Type valueType) {
return new SecondaryBinder<P>(interfaceType, valueType);
}
public class SecondaryBinder<P extends CheckedProvider> {
private final Class<P> interfaceType;
private final Type valueType;
private Class<? extends Annotation> annotationType;
private Annotation annotation;
private final List<Class<? extends Throwable>> exceptionTypes;
private final boolean valid;
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;
}
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, UniqueAnnotations.create());
binder.bind(targetKey).toInstance(target);
return to(targetKey);
}
public ScopedBindingBuilder to(Class<? extends P> targetType) {
return to(Key.get(targetType));
}
ScopedBindingBuilder toProviderMethod(ThrowingProviderMethod<?> target) {
Key<ThrowingProviderMethod> targetKey =
Key.get(ThrowingProviderMethod.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 Key<P> key = createKey();
final Provider<Result> resultProvider = binder.getProvider(resultKey);
final Provider<? extends CheckedProvider> targetProvider = binder.getProvider(targetKey);
// don't bother binding the proxy type if this is in an invalid state.
if(valid) {
binder.bind(key).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 {
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}.
*/
@SuppressWarnings({"unchecked"})
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;
}
}
@SuppressWarnings("unused")
private static long serialVersionUID = 0L;
}
}