blob: bd7f6cca05ee1fbd98c7f68da1a8b5b8db6c499d [file] [log] [blame]
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)
* @since 4.2
*/
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));
}
}