introduced functional model for dynamic proxies

This commit is contained in:
Adrian Cole 2013-01-06 23:11:14 -08:00
parent c42f59a8da
commit bd9e998b12
4 changed files with 572 additions and 0 deletions

View File

@ -0,0 +1,142 @@
/**
* Licensed to jclouds, Inc. (jclouds) under one or more
* contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. jclouds licenses this file
* to you 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 org.jclouds.reflect;
import static com.google.common.base.Objects.equal;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Predicates.notNull;
import static com.google.common.base.Throwables.propagate;
import static com.google.common.collect.Iterables.all;
import static org.jclouds.util.Throwables2.propagateIfPossible;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.jclouds.reflect.Invocation.Result;
import com.google.common.annotations.Beta;
import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.common.reflect.Invokable;
/**
* Static utilities relating to functional Java reflection.
*
* @since 1.6
*/
@Beta
public final class FunctionalReflection {
/**
* Returns a proxy instance that implements {@code interfaceType} by dispatching method invocations to
* {@code invocationFunction}. The class loader of {@code interfaceType} will be used to define the proxy class.
* <p>
* Usage example:
*
* <pre>
* httpAdapter = new Function&lt;Invocation, Result&gt;() {
* public Result apply(Invocation in) {
* try {
* HttpRequest request = parseRequest(in);
* HttpResponse response = invoke(request);
* return Result.success(parseJson(response));
* } catch (Exception e) {
* return Result.failure(e);
* }
* }
* };
*
* client = FunctionalReflection.newProxy(Client.class, httpAdapter);
* </pre>
*
* @param invocationFunction
* returns a result or a top-level exception, or result
* @throws IllegalArgumentException
* if {@code interfaceType} does not specify the type of a Java interface
* @see com.google.common.reflect.AbstractInvocationHandler#invoke(Object, Method, Object[])
* @see com.google.common.reflect.Reflection#newProxy(Class, java.lang.reflect.InvocationHandler)
*/
public static <T> T newProxy(Class<T> interfaceType, Function<Invocation, Result> invocationFunction) {
checkNotNull(interfaceType, "interfaceType");
checkNotNull(invocationFunction, "invocationFunction");
checkArgument(interfaceType.isInterface(), "%s is not an interface", interfaceType);
Object object = Proxy.newProxyInstance(interfaceType.getClassLoader(), new Class<?>[] { interfaceType },
new FunctionalInvocationHandler<T>(interfaceType, invocationFunction));
return interfaceType.cast(object);
}
private static final class FunctionalInvocationHandler<T> extends
com.google.common.reflect.AbstractInvocationHandler {
private final Class<T> interfaceType;
private final Function<Invocation, Result> invocationFunction;
private FunctionalInvocationHandler(Class<T> interfaceType, Function<Invocation, Result> invocationFunction) {
this.interfaceType = interfaceType;
this.invocationFunction = invocationFunction;
}
@Override
protected Object handleInvocation(Object proxy, Method invoked, Object[] argv) throws Throwable {
List<Object> args = Arrays.asList(argv);
if (all(args, notNull()))
args = ImmutableList.copyOf(args);
else
args = Collections.unmodifiableList(args);
Invokable<?, ?> invokable = Invokable.class.cast(Invokable.from(invoked));
// not yet support the proxy arg
Invocation invocation = Invocation.create(interfaceType, invokable, args);
Result result;
try {
result = invocationFunction.apply(invocation);
} catch (RuntimeException e) {
result = Result.fail(e);
}
if (result.getThrowable().isPresent()) {
propagateIfPossible(result.getThrowable().get(), invocation.getInvokable().getExceptionTypes());
throw propagate(result.getThrowable().get());
}
return result.getResult().orNull();
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
FunctionalInvocationHandler<?> that = FunctionalInvocationHandler.class.cast(o);
return equal(this.interfaceType, that.interfaceType)
&& equal(this.invocationFunction, that.invocationFunction);
}
@Override
public int hashCode() {
return Objects.hashCode(interfaceType, invocationFunction);
}
@Override
public String toString() {
return invocationFunction.toString();
}
}
}

View File

@ -0,0 +1,187 @@
/**
* Licensed to jclouds, Inc. (jclouds) under one or more
* contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. jclouds licenses this file
* to you 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 org.jclouds.reflect;
import static com.google.common.base.Objects.equal;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.util.List;
import org.jclouds.javax.annotation.Nullable;
import com.google.common.annotations.Beta;
import com.google.common.base.Objects;
import com.google.common.base.Objects.ToStringHelper;
import com.google.common.base.Optional;
import com.google.common.reflect.Invokable;
/**
* Context needed to call {@link com.google.common.reflect.Invokable#invoke(Object, Object...)}
*
* @author Adrian Cole
*/
@Beta
public final class Invocation {
/**
* Use this class when the invokable could be inherited. For example, a method is inherited when it cannot be
* retrieved by {@link Class#getDeclaredMethods()}, but it can be retrieved by {@link Class#getMethods()}.
*
* @param interfaceType
* type that either declared or inherited {@code invokable}, or was forwarded a call to it.
* @param args
* as these represent parameters, can contain nulls
*/
public static Invocation create(Class<?> interfaceType, Invokable<?, ?> invokable, List<Object> args) {
checkArgument(invokable.getDeclaringClass().isAssignableFrom(interfaceType), "%s isn't assignable from %s",
invokable.getDeclaringClass(), interfaceType);
return new Invocation(interfaceType, invokable, args);
}
/**
* Note: use {@link #create(Class, Invokable, List)} when the invokable was inherited.
*
* @param args
* as these represent parameters, can contain nulls
*/
public static Invocation create(Invokable<?, ?> invokable, List<Object> args) {
return new Invocation(invokable.getDeclaringClass(), invokable, args);
}
private final Class<?> interfaceType;
private final Invokable<?, ?> invokable;
private final List<Object> args;
private Invocation(Class<?> interfaceType, Invokable<?, ?> invokable, List<Object> args) {
this.interfaceType = checkNotNull(interfaceType, "interfaceType");
this.invokable = checkNotNull(invokable, "invokable");
this.args = checkNotNull(args, "args");
}
/**
* different than {@link Invokable#getDeclaringClass()} when {@link #getInvokable()} is a member of a class it was
* not declared in.
*/
public Class<?> getInterfaceType() {
return interfaceType;
}
/**
* what we can invoke
*/
public Invokable<?, ?> getInvokable() {
return invokable;
}
/**
* arguments applied to {@link #getInvokable()} during {@link Invokable#invoke(Object, Object...)}
*
* @param args
* as these represent parameters, can contain nulls
*/
public List<Object> getArgs() {
return args;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Invocation that = Invocation.class.cast(o);
return equal(this.interfaceType, that.interfaceType) && equal(this.invokable, that.invokable)
&& equal(this.args, that.args);
}
@Override
public int hashCode() {
return Objects.hashCode(interfaceType, invokable, args);
}
@Override
public String toString() {
return Objects.toStringHelper("").omitNullValues().add("interfaceType", interfaceType)
.add("invokable", invokable).add("args", args.size() != 0 ? args : null).toString();
}
/**
* result of an invocation which is either successful or failed, but not both.
*/
@Beta
public final static class Result {
public static Result success(@Nullable Object result) {
return new Result(Optional.fromNullable(result), Optional.<Throwable> absent());
}
public static Result fail(Throwable throwable) {
return new Result(Optional.absent(), Optional.of(throwable));
}
private final Optional<Object> result;
private final Optional<Throwable> throwable;
private Result(Optional<Object> result, Optional<Throwable> throwable) {
this.result = checkNotNull(result, "result");
this.throwable = checkNotNull(throwable, "throwable");
}
/**
* result of{@link Invokable#invoke(Object, Object...)}
*/
public Optional<Object> getResult() {
return result;
}
/**
* throwable received during {@link Invokable#invoke(Object, Object...)}
*/
public Optional<Throwable> getThrowable() {
return throwable;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Result that = Result.class.cast(o);
return equal(this.result.orNull(), that.result.orNull())
&& equal(this.throwable.orNull(), that.throwable.orNull());
}
@Override
public int hashCode() {
return Objects.hashCode(result.orNull(), throwable.orNull());
}
@Override
public String toString() {
return string().toString();
}
protected ToStringHelper string() {
return Objects.toStringHelper("").omitNullValues().add("result", result.orNull())
.add("throwable", throwable.orNull());
}
}
}

View File

@ -0,0 +1,84 @@
/**
* Licensed to jclouds, Inc. (jclouds) under one or more
* contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. jclouds licenses this file
* to you 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 org.jclouds.reflect;
import static com.google.common.base.Objects.equal;
import static com.google.common.base.Preconditions.checkNotNull;
import org.jclouds.javax.annotation.Nullable;
import com.google.common.annotations.Beta;
import com.google.common.base.Objects;
import com.google.common.base.Objects.ToStringHelper;
import com.google.common.base.Optional;
/**
* Holds the context of a successful call to {@link com.google.common.reflect.Invokable#invoke(Object, Object...)}
*
* @author Adrian Cole
*/
@Beta
public final class InvocationSuccess {
public static InvocationSuccess create(Invocation invocation, @Nullable Object result) {
return new InvocationSuccess(invocation, Optional.fromNullable(result));
}
private final Invocation invocation;
private final Optional<Object> result;
private InvocationSuccess(Invocation invocation, Optional<Object> result) {
this.invocation = checkNotNull(invocation, "invocation");
this.result = checkNotNull(result, "result");
}
/**
* what was invocation
*/
public Invocation getInvocation() {
return invocation;
}
public Optional<Object> getResult() {
return result;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
InvocationSuccess that = InvocationSuccess.class.cast(o);
return equal(this.invocation, that.invocation) && equal(this.result.orNull(), that.result.orNull());
}
@Override
public int hashCode() {
return Objects.hashCode(invocation, result.orNull());
}
@Override
public String toString() {
return string().toString();
}
protected ToStringHelper string() {
return Objects.toStringHelper("").omitNullValues().add("invocation", invocation).add("result", result.orNull());
}
}

View File

@ -0,0 +1,159 @@
/**
* Licensed to jclouds, Inc. (jclouds) under one or more
* contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. jclouds licenses this file
* to you 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 org.jclouds.reflect;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import java.io.Closeable;
import java.io.IOException;
import java.util.Set;
import java.util.SortedSet;
import java.util.concurrent.TimeoutException;
import org.jclouds.reflect.Invocation.Result;
import org.testng.annotations.Test;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
/**
*
* @author Adrian Cole
*/
@Test(singleThreaded = true)
public class FunctionalReflectionTest {
/**
* a method only has reference to its declaring type, not the interface specified to the proxy. this shows how to get
* access to the actual proxied interface
*/
@SuppressWarnings("unchecked")
public void testCanAccessInterfaceTypeInsideFunction() {
final Function<Invocation, Result> test = new Function<Invocation, Result>() {
public Result apply(Invocation e) {
assertEquals(e.getInvokable().getDeclaringClass(), Set.class);
assertEquals(e.getInterfaceType(), SortedSet.class);
return Result.success(true);
}
};
FunctionalReflection.newProxy(SortedSet.class, test).add(null);
}
@SuppressWarnings("unchecked")
@Test(expectedExceptions = UnsupportedOperationException.class)
public void testNullArgsAreAllowedAndUnmodifiable() {
final Function<Invocation, Result> test = new Function<Invocation, Result>() {
public Result apply(Invocation e) {
assertNotNull(e.getArgs());
assertNull(e.getArgs().get(0));
e.getArgs().add("foo");
throw new AssertionError("shouldn't be able to mutate the list!");
}
};
FunctionalReflection.newProxy(Set.class, test).add(null);
}
@SuppressWarnings("unchecked")
@Test(expectedExceptions = UnsupportedOperationException.class)
public void testImmutableListWhenArgsAreNotNull() {
final Function<Invocation, Result> test = new Function<Invocation, Result>() {
public Result apply(Invocation e) {
assertNotNull(e.getArgs());
assertTrue(e.getArgs() instanceof ImmutableList);
assertEquals(e.getArgs().get(0), "foo");
e.getArgs().add("bar");
throw new AssertionError("shouldn't be able to mutate the list!");
}
};
FunctionalReflection.newProxy(Set.class, test).add("foo");
}
@Test(expectedExceptions = IOException.class, expectedExceptionsMessageRegExp = "io")
public void testPropagatesDeclaredException() throws IOException {
final Function<Invocation, Result> test = new Function<Invocation, Result>() {
public Result apply(Invocation e) {
return Result.fail(new IOException("io"));
}
};
Closeable closeable = FunctionalReflection.newProxy(Closeable.class, test);
closeable.close();
}
/**
* for example, someone could have enabled assertions, or there could be a recoverable ServiceConfigurationError
*/
@Test(expectedExceptions = AssertionError.class, expectedExceptionsMessageRegExp = "assert")
public void testPropagatesError() throws IOException {
final Function<Invocation, Result> test = new Function<Invocation, Result>() {
public Result apply(Invocation e) {
return Result.fail(new AssertionError("assert"));
}
};
Closeable closeable = FunctionalReflection.newProxy(Closeable.class, test);
closeable.close();
}
// TODO: coerce things like this to UncheckedTimeoutException and friends
@Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = ".*timeout")
public void testWrapsDeclaredException() throws IOException {
final Function<Invocation, Result> test = new Function<Invocation, Result>() {
public Result apply(Invocation e) {
return Result.fail(new TimeoutException("timeout"));
}
};
Closeable closeable = FunctionalReflection.newProxy(Closeable.class, test);
closeable.close();
}
public void testToStringEqualsFunction() {
final Function<Invocation, Result> test = new Function<Invocation, Result>() {
public Result apply(Invocation e) {
return Result.success("foo");
}
public String toString() {
return "bar";
}
};
Closeable closeable = FunctionalReflection.newProxy(Closeable.class, test);
assertEquals(closeable.toString(), "bar");
}
public void testHashCodeDifferentiatesOnInterface() {
final Function<Invocation, Result> test = new Function<Invocation, Result>() {
public Result apply(Invocation e) {
return Result.success(null);
}
public int hashCode() {
return 1111;
}
};
Appendable appendable1 = FunctionalReflection.newProxy(Appendable.class, test);
Appendable appendable2 = FunctionalReflection.newProxy(Appendable.class, test);
assertEquals(appendable1.hashCode(), appendable2.hashCode());
Closeable closeable = FunctionalReflection.newProxy(Closeable.class, test);
assertNotEquals(appendable1.hashCode(), closeable.hashCode());
}
}