package com.google.inject.throwingproviders; import static com.google.inject.throwingproviders.ProviderChecker.checkInterface; import com.google.common.base.Optional; import com.google.inject.TypeLiteral; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.List; import javax.annotation.Nullable; /** * Static utility methods for creating and working with instances of {@link CheckedProvider}. * * @author eatnumber1@google.com (Russ Harmon) */ public final class CheckedProviders { private abstract static class CheckedProviderInvocationHandler<T> implements InvocationHandler { private boolean isGetMethod(Method method) { // Since Java does not allow multiple methods with the same name and number & type of // arguments, this is all we need to check to see if it is an overriding method of // CheckedProvider#get(). return method.getName().equals("get") && method.getParameterTypes().length == 0; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getDeclaringClass() == Object.class) { return method.invoke(this, args); } else if (isGetMethod(method)) { return invokeGet(proxy, method); } throw new UnsupportedOperationException( String.format( "Unsupported method <%s> with args <%s> invoked on <%s>", method, Arrays.toString(args), proxy)); } protected abstract T invokeGet(Object proxy, Method method) throws Throwable; @Override public abstract String toString(); } private static final class ReturningHandler<T> extends CheckedProviderInvocationHandler<T> { private final T returned; ReturningHandler(T returned) { this.returned = returned; } @Override protected T invokeGet(Object proxy, Method method) throws Throwable { return returned; } @Override public String toString() { return String.format("generated CheckedProvider returning <%s>", returned); } } private static final class ThrowingHandler extends CheckedProviderInvocationHandler<Object> { private final Constructor<? extends Throwable> throwableCtor; private final String typeName; private ThrowingHandler(Constructor<? extends Throwable> throwableCtor, String typeName) { this.throwableCtor = throwableCtor; this.typeName = typeName; this.throwableCtor.setAccessible(true); } static ThrowingHandler forClass(Class<? extends Throwable> throwable) { try { return new ThrowingHandler(throwable.getDeclaredConstructor(), throwable.getName()); } catch (NoSuchMethodException e) { // This should have been caught by checkThrowable throw new AssertionError( String.format( "Throwable <%s> does not have a no-argument constructor", throwable.getName())); } } @Override protected Object invokeGet(Object proxy, Method method) throws Throwable { throw this.throwableCtor.newInstance(); } @Override public String toString() { return String.format("generated CheckedProvider throwing <%s>", this.typeName); } } private CheckedProviders() {} private static <T, P extends CheckedProvider<? super T>> P generateProvider( Class<P> providerType, Optional<T> value, InvocationHandler handler) { checkInterface(providerType, getClassOptional(value)); Object proxy = Proxy.newProxyInstance( providerType.getClassLoader(), new Class<?>[] {providerType}, handler); @SuppressWarnings("unchecked") // guaranteed by the newProxyInstance API P proxyP = (P) proxy; return proxyP; } private static <T, P extends CheckedProvider<? super T>> P generateProvider( TypeLiteral<P> providerType, Optional<T> value, InvocationHandler handler) { // TODO(eatnumber1): Understand why TypeLiteral#getRawType returns a Class<? super T> rather // than a Class<T> and remove this unsafe cast. Class<P> providerRaw = (Class) providerType.getRawType(); return generateProvider(providerRaw, value, handler); } private static Optional<Class<?>> getClassOptional(Optional<?> value) { if (!value.isPresent()) { return Optional.absent(); } return Optional.<Class<?>>of(value.get().getClass()); } /** * Returns a {@link CheckedProvider} which always provides {@code instance}. * * <p>The provider type passed as {@code providerType} must be an interface. Calls to methods * other than {@link CheckedProvider#get} will throw {@link UnsupportedOperationException}. * * @param providerType the type of the {@link CheckedProvider} to return * @param instance the instance that should always be provided */ public static <T, P extends CheckedProvider<? super T>> P of( TypeLiteral<P> providerType, @Nullable T instance) { return generateProvider( providerType, Optional.fromNullable(instance), new ReturningHandler<T>(instance)); } /** * Returns a {@link CheckedProvider} which always provides {@code instance}. * * @param providerType the type of the {@link CheckedProvider} to return * @param instance the instance that should always be provided * @see #of(TypeLiteral, T) */ public static <T, P extends CheckedProvider<? super T>> P of( Class<P> providerType, @Nullable T instance) { return of(TypeLiteral.get(providerType), instance); } /** * Returns a {@link CheckedProvider} which always throws exceptions. * * <p>This method uses the nullary (no argument) constructor of {@code throwable} to create a new * instance of the given {@link Throwable} on each method invocation which is then thrown * immediately. * * <p>See {@link #of(TypeLiteral, T)} for more information. * * @param providerType the type of the {@link CheckedProvider} to return * @param throwable the type of the {@link Throwable} to throw * @see #of(TypeLiteral, T) */ public static <T, P extends CheckedProvider<? super T>> P throwing( TypeLiteral<P> providerType, Class<? extends Throwable> throwable) { // TODO(eatnumber1): Understand why TypeLiteral#getRawType returns a Class<? super T> rather // than a Class<T> and remove this unsafe cast. Class<P> providerRaw = (Class) providerType.getRawType(); checkThrowable(providerRaw, throwable); return generateProvider( providerType, Optional.<T>absent(), ThrowingHandler.forClass(throwable)); } /** * Returns a {@link CheckedProvider} which always throws exceptions. * * @param providerType the type of the {@link CheckedProvider} to return * @param throwable the type of the {@link Throwable} to throw * @see #throwing(TypeLiteral, Class) */ public static <T, P extends CheckedProvider<? super T>> P throwing( Class<P> providerType, Class<? extends Throwable> throwable) { return throwing(TypeLiteral.get(providerType), throwable); } private static boolean isCheckedException(Class<? extends Throwable> thrownType) { return Exception.class.isAssignableFrom(thrownType) && !RuntimeException.class.isAssignableFrom(thrownType); } private static void checkThrowable( Class<? extends CheckedProvider<?>> providerType, Class<? extends Throwable> thrownType) { try { thrownType.getDeclaredConstructor(); } catch (NoSuchMethodException e) { throw new IllegalArgumentException( String.format( "Thrown exception <%s> must have a no-argument constructor", thrownType.getName()), e); } if (!isCheckedException(thrownType)) { return; } Method getMethod; try { getMethod = providerType.getMethod("get"); } catch (NoSuchMethodException e) { throw new IllegalArgumentException( String.format("Provider class <%s> must have a get() method", providerType.getName()), e); } @SuppressWarnings("unchecked") // guaranteed by getExceptionTypes List<Class<? extends Throwable>> exceptionTypes = Arrays.asList((Class<? extends Throwable>[]) getMethod.getExceptionTypes()); if (exceptionTypes.size() == 0) { return; } // Check if the thrown exception is declared to be thrown in the method signature. for (Class<? extends Throwable> exceptionType : exceptionTypes) { if (exceptionType.isAssignableFrom(thrownType)) { return; } } throw new IllegalArgumentException( String.format( "Thrown exception <%s> is not declared to be thrown by <%s>", thrownType.getName(), getMethod)); } }