/** * 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 jmourits@google.com (Jerome Mourits) * @author jessewilson@google.com (Jesse Wilson) * @author sameb@google.com (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 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 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 targetType) { return to(Key.get(targetType)); } /** @since 4.0 */ public ScopedBindingBuilder providing(Class cxtorClass) { return providing(TypeLiteral.get(cxtorClass)); } /** @since 4.0 */ @SuppressWarnings("unchecked") // safe because this is the cxtor of the literal public ScopedBindingBuilder providing(TypeLiteral cxtorLiteral) { // Find a constructor that has @ThrowingInject. Constructor cxtor = CheckedProvideUtils.findThrowingConstructor(cxtorLiteral, binder); final Provider typeProvider; final Key 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 targetKey) { checkNotNull(targetKey, "targetKey"); return toInternal((Key>)targetKey); } private ScopedBindingBuilder toInternal(final Key> 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> 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> targetKey, final Provider> targetProvider) { return new ProviderWithDependencies() { @Override public Result get() { try { return Result.forValue(targetProvider.get().get()); } catch (Exception e) { for (Class 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(); } } }