/** * 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 CheckedProviderMethodsModule.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 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> 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(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 { 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; } }