summaryrefslogtreecommitdiffstats
path: root/dx
diff options
context:
space:
mode:
authorHugo Hudson <hugohudson@google.com>2011-12-16 18:06:39 +0000
committerHugo Hudson <hugohudson@google.com>2011-12-21 03:59:00 +0000
commit26f957278b34144a3f90989ccddb0102fc1fdd62 (patch)
tree14b9e490fe1629678a089a3a018153a06052c395 /dx
parentf8fec55f4689b0c54529b2bbf4e25b0c7cfbbe1e (diff)
downloadandroid_dalvik-26f957278b34144a3f90989ccddb0102fc1fdd62.tar.gz
android_dalvik-26f957278b34144a3f90989ccddb0102fc1fdd62.tar.bz2
android_dalvik-26f957278b34144a3f90989ccddb0102fc1fdd62.zip
Introduces ProxyBuilder and tests.
- ProxyBuilder is to concrete classes what java.lang.reflect.Proxy is to interfaces. - Uses a builder pattern to make specifying of the various (optional) parameters easier. - Creates a concrete subclass of the supplied input class whose implementation delegates to the given invocation handler. - Also provides a fix for the Code#loadConstant method to allow loading null values. Change-Id: I3ca6a98b91c64466df03120bc85f095365250aca
Diffstat (limited to 'dx')
-rw-r--r--dx/junit-tests/com/android/dx/gen/ProxyBuilderTest.java536
-rw-r--r--dx/src/com/android/dx/gen/Code.java4
-rw-r--r--dx/src/com/android/dx/gen/DexCacheException.java28
-rw-r--r--dx/src/com/android/dx/gen/ProxyBuilder.java667
4 files changed, 1234 insertions, 1 deletions
diff --git a/dx/junit-tests/com/android/dx/gen/ProxyBuilderTest.java b/dx/junit-tests/com/android/dx/gen/ProxyBuilderTest.java
new file mode 100644
index 000000000..070031cbc
--- /dev/null
+++ b/dx/junit-tests/com/android/dx/gen/ProxyBuilderTest.java
@@ -0,0 +1,536 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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.android.dx.gen;
+
+import junit.framework.AssertionFailedError;
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.UndeclaredThrowableException;
+import java.util.Random;
+
+public class ProxyBuilderTest extends TestCase {
+ private FakeInvocationHandler fakeHandler = new FakeInvocationHandler();
+
+ public static class SimpleClass {
+ public String simpleMethod() {
+ throw new AssertionFailedError();
+ }
+ }
+
+ public void testExampleOperation() throws Throwable {
+ fakeHandler.setFakeResult("expected");
+ SimpleClass proxy = proxyFor(SimpleClass.class).build();
+ assertEquals("expected", proxy.simpleMethod());
+ }
+
+ public static class ConstructorTakesArguments {
+ private final String argument;
+
+ public ConstructorTakesArguments(String arg) {
+ argument = arg;
+ }
+
+ public String method() {
+ throw new AssertionFailedError();
+ }
+ }
+
+ public void testConstruction_SucceedsIfCorrectArgumentsProvided() throws Throwable {
+ ConstructorTakesArguments proxy = proxyFor(ConstructorTakesArguments.class)
+ .constructorArgTypes(String.class)
+ .constructorArgValues("hello")
+ .build();
+ assertEquals("hello", proxy.argument);
+ proxy.method();
+ }
+
+ public void testConstruction_FailsWithWrongNumberOfArguments() throws Throwable {
+ try {
+ proxyFor(ConstructorTakesArguments.class).build();
+ fail();
+ } catch (IllegalArgumentException expected) {}
+ }
+
+ public void testClassIsNotAccessbile_FailsWithUnsupportedOperationException() throws Exception {
+ class MethodVisibilityClass {
+ }
+ try {
+ proxyFor(MethodVisibilityClass.class).build();
+ fail();
+ } catch (UnsupportedOperationException expected) {}
+ }
+
+ private static class PrivateVisibilityClass {
+ }
+
+ public void testPrivateClass_FailsWithUnsupportedOperationException() throws Exception {
+ try {
+ proxyFor(PrivateVisibilityClass.class).build();
+ fail();
+ } catch (UnsupportedOperationException expected) {}
+ }
+
+ protected static class ProtectedVisibilityClass {
+ public String foo() {
+ throw new AssertionFailedError();
+ }
+ }
+
+ public void testProtectedVisibility_WorksFine() throws Exception {
+ assertEquals("fake result", proxyFor(ProtectedVisibilityClass.class).build().foo());
+ }
+
+ public static class HasFinalMethod {
+ public String nonFinalMethod() {
+ return "non-final method";
+ }
+
+ public final String finalMethod() {
+ return "final method";
+ }
+ }
+
+ public void testCanProxyClassesWithFinalMethods_WillNotCallTheFinalMethod() throws Throwable {
+ HasFinalMethod proxy = proxyFor(HasFinalMethod.class).build();
+ assertEquals("final method", proxy.finalMethod());
+ assertEquals("fake result", proxy.nonFinalMethod());
+ }
+
+ public static class HasPrivateMethod {
+ private String result() {
+ return "expected";
+ }
+ }
+
+ public void testProxyingPrivateMethods_NotIntercepted() throws Throwable {
+ assertEquals("expected", proxyFor(HasPrivateMethod.class).build().result());
+ }
+
+ public static class HasPackagePrivateMethod {
+ String result() {
+ throw new AssertionFailedError();
+ }
+ }
+
+ public void testProxyingPackagePrivateMethods_AreIntercepted() throws Throwable {
+ assertEquals("fake result", proxyFor(HasPackagePrivateMethod.class).build().result());
+ }
+
+ public static class HasProtectedMethod {
+ protected String result() {
+ throw new AssertionFailedError();
+ }
+ }
+
+ public void testProxyingProtectedMethods_AreIntercepted() throws Throwable {
+ assertEquals("fake result", proxyFor(HasProtectedMethod.class).build().result());
+ }
+
+ public static class HasVoidMethod {
+ public void dangerousMethod() {
+ fail();
+ }
+ }
+
+ public void testVoidMethod_ShouldNotThrowRuntimeException() throws Throwable {
+ proxyFor(HasVoidMethod.class).build().dangerousMethod();
+ }
+
+ public void testObjectMethodsAreAlsoProxied() throws Throwable {
+ Object proxy = proxyFor(Object.class).build();
+ fakeHandler.setFakeResult("mystring");
+ assertEquals("mystring", proxy.toString());
+ fakeHandler.setFakeResult(-1);
+ assertEquals(-1, proxy.hashCode());
+ fakeHandler.setFakeResult(false);
+ assertEquals(false, proxy.equals(proxy));
+ }
+
+ public static class AllPrimitiveMethods {
+ public boolean getBoolean() { return true; }
+ public int getInt() { return 1; }
+ public byte getByte() { return 2; }
+ public long getLong() { return 3L; }
+ public short getShort() { return 4; }
+ public float getFloat() { return 5f; }
+ public double getDouble() { return 6.0; }
+ public char getChar() { return 'c'; }
+ }
+
+ public void testAllPrimitiveReturnTypes() throws Throwable {
+ AllPrimitiveMethods proxy = proxyFor(AllPrimitiveMethods.class).build();
+ fakeHandler.setFakeResult(false);
+ assertEquals(false, proxy.getBoolean());
+ fakeHandler.setFakeResult(8);
+ assertEquals(8, proxy.getInt());
+ fakeHandler.setFakeResult((byte) 9);
+ assertEquals(9, proxy.getByte());
+ fakeHandler.setFakeResult(10L);
+ assertEquals(10, proxy.getLong());
+ fakeHandler.setFakeResult((short) 11);
+ assertEquals(11, proxy.getShort());
+ fakeHandler.setFakeResult(12f);
+ assertEquals(12f, proxy.getFloat());
+ fakeHandler.setFakeResult(13.0);
+ assertEquals(13.0, proxy.getDouble());
+ fakeHandler.setFakeResult('z');
+ assertEquals('z', proxy.getChar());
+ }
+
+ public static class PassThroughAllPrimitives {
+ public boolean getBoolean(boolean input) { return input; }
+ public int getInt(int input) { return input; }
+ public byte getByte(byte input) { return input; }
+ public long getLong(long input) { return input; }
+ public short getShort(short input) { return input; }
+ public float getFloat(float input) { return input; }
+ public double getDouble(double input) { return input; }
+ public char getChar(char input) { return input; }
+ public String getString(String input) { return input; }
+ public Object getObject(Object input) { return input; }
+ public void getNothing() {}
+ }
+
+ public static class InvokeSuperHandler implements InvocationHandler {
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ return ProxyBuilder.callSuper(proxy, method, args);
+ }
+ }
+
+ public void testPassThroughWorksForAllPrimitives() throws Exception {
+ PassThroughAllPrimitives proxy = proxyFor(PassThroughAllPrimitives.class)
+ .handler(new InvokeSuperHandler())
+ .build();
+ assertEquals(false, proxy.getBoolean(false));
+ assertEquals(true, proxy.getBoolean(true));
+ assertEquals(0, proxy.getInt(0));
+ assertEquals(1, proxy.getInt(1));
+ assertEquals((byte) 2, proxy.getByte((byte) 2));
+ assertEquals((byte) 3, proxy.getByte((byte) 3));
+ assertEquals(4L, proxy.getLong(4L));
+ assertEquals(5L, proxy.getLong(5L));
+ assertEquals((short) 6, proxy.getShort((short) 6));
+ assertEquals((short) 7, proxy.getShort((short) 7));
+ assertEquals(8f, proxy.getFloat(8f));
+ assertEquals(9f, proxy.getFloat(9f));
+ assertEquals(10.0, proxy.getDouble(10.0));
+ assertEquals(11.0, proxy.getDouble(11.0));
+ assertEquals('a', proxy.getChar('a'));
+ assertEquals('b', proxy.getChar('b'));
+ assertEquals("asdf", proxy.getString("asdf"));
+ assertEquals("qwer", proxy.getString("qwer"));
+ assertEquals(null, proxy.getString(null));
+ Object a = new Object();
+ assertEquals(a, proxy.getObject(a));
+ assertEquals(null, proxy.getObject(null));
+ proxy.getNothing();
+ }
+
+ public static class ExtendsAllPrimitiveMethods extends AllPrimitiveMethods {
+ public int example() { return 0; }
+ }
+
+ public void testProxyWorksForSuperclassMethodsAlso() throws Throwable {
+ ExtendsAllPrimitiveMethods proxy = proxyFor(ExtendsAllPrimitiveMethods.class).build();
+ fakeHandler.setFakeResult(99);
+ assertEquals(99, proxy.example());
+ assertEquals(99, proxy.getInt());
+ assertEquals(99, proxy.hashCode());
+ }
+
+ public static class HasOddParams {
+ public long method(int first, Integer second) {
+ throw new AssertionFailedError();
+ }
+ }
+
+ public void testMixingBoxedAndUnboxedParams() throws Throwable {
+ HasOddParams proxy = proxyFor(HasOddParams.class).build();
+ fakeHandler.setFakeResult(99L);
+ assertEquals(99L, proxy.method(1, Integer.valueOf(2)));
+ }
+
+ public static class SingleInt {
+ public String getString(int value) {
+ throw new AssertionFailedError();
+ }
+ }
+
+ public void testSinglePrimitiveParameter() throws Throwable {
+ InvocationHandler handler = new InvocationHandler() {
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ return "asdf" + ((Integer) args[0]).intValue();
+ }
+ };
+ assertEquals("asdf1", proxyFor(SingleInt.class).handler(handler).build().getString(1));
+ }
+
+ public static class TwoConstructors {
+ private final String string;
+
+ public TwoConstructors() {
+ string = "no-arg";
+ }
+
+ public TwoConstructors(boolean unused) {
+ string = "one-arg";
+ }
+ }
+
+ public void testNoConstructorArguments_CallsNoArgConstructor() throws Throwable {
+ TwoConstructors twoConstructors = proxyFor(TwoConstructors.class).build();
+ assertEquals("no-arg", twoConstructors.string);
+ }
+
+ public void testWithoutInvocationHandler_ThrowsIllegalArgumentException() throws Throwable {
+ try {
+ ProxyBuilder.forClass(TwoConstructors.class)
+ .dexCache(DexGeneratorTest.getDataDirectory())
+ .build();
+ fail();
+ } catch (IllegalArgumentException expected) {}
+ }
+
+ public static class HardToConstructCorrectly {
+ public HardToConstructCorrectly() { fail(); }
+ public HardToConstructCorrectly(Runnable ignored) { fail(); }
+ public HardToConstructCorrectly(Exception ignored) { fail(); }
+ public HardToConstructCorrectly(Boolean ignored) { /* safe */ }
+ public HardToConstructCorrectly(Integer ignored) { fail(); }
+ }
+
+ public void testHardToConstruct_WorksIfYouSpecifyTheConstructorCorrectly() throws Throwable {
+ proxyFor(HardToConstructCorrectly.class)
+ .constructorArgTypes(Boolean.class)
+ .constructorArgValues(true)
+ .build();
+ }
+
+ public void testHardToConstruct_EvenWorksWhenArgsAreAmbiguous() throws Throwable {
+ proxyFor(HardToConstructCorrectly.class)
+ .constructorArgTypes(Boolean.class)
+ .constructorArgValues(new Object[] { null })
+ .build();
+ }
+
+ public void testHardToConstruct_DoesNotInferTypesFromValues() throws Throwable {
+ try {
+ proxyFor(HardToConstructCorrectly.class)
+ .constructorArgValues(true)
+ .build();
+ fail();
+ } catch (IllegalArgumentException expected) {}
+ }
+
+ public void testDefaultProxyHasSuperMethodToAccessOriginal() throws Exception {
+ Object objectProxy = proxyFor(Object.class).build();
+ assertNotNull(objectProxy.getClass().getMethod("super_hashCode"));
+ }
+
+ public static class PrintsOddAndValue {
+ public String method(int value) {
+ return "odd " + value;
+ }
+ }
+
+ public void testSometimesDelegateToSuper() throws Exception {
+ InvocationHandler delegatesOddValues = new InvocationHandler() {
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ if (method.getName().equals("method")) {
+ int intValue = ((Integer) args[0]).intValue();
+ if (intValue % 2 == 0) {
+ return "even " + intValue;
+ }
+ }
+ return ProxyBuilder.callSuper(proxy, method, args);
+ }
+ };
+ PrintsOddAndValue proxy = proxyFor(PrintsOddAndValue.class)
+ .handler(delegatesOddValues)
+ .build();
+ assertEquals("even 0", proxy.method(0));
+ assertEquals("odd 1", proxy.method(1));
+ assertEquals("even 2", proxy.method(2));
+ assertEquals("odd 3", proxy.method(3));
+ }
+
+ public static class DoubleReturn {
+ public double getValue() {
+ return 2.0;
+ }
+ }
+
+ public void testUnboxedResult() throws Exception {
+ fakeHandler.fakeResult = 2.0;
+ assertEquals(2.0, proxyFor(DoubleReturn.class).build().getValue());
+ }
+
+ public static void staticMethod() {
+ }
+
+ public void testDoesNotOverrideStaticMethods() throws Exception {
+ // Method should exist on this test class itself.
+ ProxyBuilderTest.class.getDeclaredMethod("staticMethod");
+ // Method should not exist on the subclass.
+ try {
+ proxyFor(ProxyBuilderTest.class).build().getClass().getDeclaredMethod("staticMethod");
+ fail();
+ } catch (NoSuchMethodException expected) {}
+ }
+
+ public void testIllegalCacheDirectory() throws Exception {
+ try {
+ proxyFor(Object.class).dexCache(new File("//////")).build();
+ fail();
+ } catch (DexCacheException expected) {}
+ }
+
+ public void testInvalidConstructorSpecification() throws Exception {
+ try {
+ proxyFor(Object.class)
+ .constructorArgTypes(String.class, Boolean.class)
+ .constructorArgValues("asdf", true)
+ .build();
+ fail();
+ } catch (IllegalArgumentException expected) {}
+ }
+
+ public static abstract class AbstractClass {
+ public abstract Object getValue();
+ }
+
+ public void testAbstractClassBehaviour() throws Exception {
+ assertEquals("fake result", proxyFor(AbstractClass.class).build().getValue());
+ }
+
+ public static class CtorHasDeclaredException {
+ public CtorHasDeclaredException() throws IOException {
+ throw new IOException();
+ }
+ }
+
+ public static class CtorHasRuntimeException {
+ public CtorHasRuntimeException() {
+ throw new RuntimeException("my message");
+ }
+ }
+
+ public static class CtorHasError {
+ public CtorHasError() {
+ throw new Error("my message again");
+ }
+ }
+
+ public void testParentConstructorThrowsDeclaredException() throws Exception {
+ try {
+ proxyFor(CtorHasDeclaredException.class).build();
+ fail();
+ } catch (UndeclaredThrowableException expected) {
+ assertTrue(expected.getCause() instanceof IOException);
+ }
+ try {
+ proxyFor(CtorHasRuntimeException.class).build();
+ fail();
+ } catch (RuntimeException expected) {
+ assertEquals("my message", expected.getMessage());
+ }
+ try {
+ proxyFor(CtorHasError.class).build();
+ fail();
+ } catch (Error expected) {
+ assertEquals("my message again", expected.getMessage());
+ }
+ }
+
+ public void testGetInvocationHandler_NormalOperation() throws Exception {
+ Object proxy = proxyFor(Object.class).build();
+ assertSame(fakeHandler, ProxyBuilder.getInvocationHandler(proxy));
+ }
+
+ public void testGetInvocationHandler_NotAProxy() {
+ try {
+ ProxyBuilder.getInvocationHandler(new Object());
+ fail();
+ } catch (IllegalArgumentException expected) {}
+ }
+
+ public static class ReturnsObject {
+ public Object getValue() {
+ return new Object();
+ }
+ }
+
+ public static class ReturnsString extends ReturnsObject {
+ @Override
+ public String getValue() {
+ return "a string";
+ }
+ }
+
+ public void testCovariantReturnTypes_NormalBehaviour() throws Exception {
+ String expected = "some string";
+ fakeHandler.setFakeResult(expected);
+ assertSame(expected, proxyFor(ReturnsObject.class).build().getValue());
+ assertSame(expected, proxyFor(ReturnsString.class).build().getValue());
+ }
+
+ public void testCovariantReturnTypes_WrongReturnType() throws Exception {
+ try {
+ fakeHandler.setFakeResult(new Object());
+ proxyFor(ReturnsString.class).build().getValue();
+ fail();
+ } catch (ClassCastException expected) {}
+ }
+
+ public void testCaching_ShouldWork() {
+ // TODO: We're not supporting caching yet. But we should as soon as possible.
+ fail();
+ }
+
+ public void testSubclassOfRandom() throws Exception {
+ proxyFor(Random.class)
+ .handler(new InvokeSuperHandler())
+ .build();
+ }
+
+ /** Simple helper to add the most common args for this test to the proxy builder. */
+ private <T> ProxyBuilder<T> proxyFor(Class<T> clazz) throws Exception {
+ return ProxyBuilder.forClass(clazz)
+ .handler(fakeHandler)
+ .dexCache(DexGeneratorTest.getDataDirectory());
+ }
+
+ private static class FakeInvocationHandler implements InvocationHandler {
+ private Object fakeResult = "fake result";
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ return fakeResult;
+ }
+
+ public void setFakeResult(Object result) {
+ fakeResult = result;
+ }
+ }
+}
diff --git a/dx/src/com/android/dx/gen/Code.java b/dx/src/com/android/dx/gen/Code.java
index b44d01cdd..3868cd3b5 100644
--- a/dx/src/com/android/dx/gen/Code.java
+++ b/dx/src/com/android/dx/gen/Code.java
@@ -288,7 +288,9 @@ public final class Code {
// instructions: constants
public <T> void loadConstant(Local<T> target, T value) {
- Rop rop = Rops.opConst(target.type.ropType);
+ Rop rop = value == null
+ ? Rops.CONST_OBJECT_NOTHROW
+ : Rops.opConst(target.type.ropType);
if (rop.getBranchingness() == BRANCH_NONE) {
addInstruction(new PlainCstInsn(rop, sourcePosition, target.spec(),
RegisterSpecList.EMPTY, Constants.getConstant(value)));
diff --git a/dx/src/com/android/dx/gen/DexCacheException.java b/dx/src/com/android/dx/gen/DexCacheException.java
new file mode 100644
index 000000000..560844098
--- /dev/null
+++ b/dx/src/com/android/dx/gen/DexCacheException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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.android.dx.gen;
+
+import java.io.IOException;
+
+/** Thrown when there is an IOException when writing to the dex cache directory. */
+public final class DexCacheException extends RuntimeException {
+ private static final long serialVersionUID = 0L;
+
+ public DexCacheException(IOException cause) {
+ super(cause);
+ }
+}
diff --git a/dx/src/com/android/dx/gen/ProxyBuilder.java b/dx/src/com/android/dx/gen/ProxyBuilder.java
new file mode 100644
index 000000000..852fe666f
--- /dev/null
+++ b/dx/src/com/android/dx/gen/ProxyBuilder.java
@@ -0,0 +1,667 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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.android.dx.gen;
+
+import static com.android.dx.rop.code.AccessFlags.ACC_CONSTRUCTOR;
+import static com.android.dx.rop.code.AccessFlags.ACC_PRIVATE;
+import static com.android.dx.rop.code.AccessFlags.ACC_PUBLIC;
+import static com.android.dx.rop.code.AccessFlags.ACC_STATIC;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.UndeclaredThrowableException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Creates dynamic proxies of concrete classes.
+ * <p>
+ * This is similar to the {@code java.lang.reflect.Proxy} class, but works for classes instead of
+ * interfaces.
+ * <h3>Example</h3>
+ * The following example demonstrates the creation of a dynamic proxy for {@code java.util.Random}
+ * which will always return 4 when asked for integers, and which logs method calls to every method.
+ * <pre>
+ * InvocationHandler handler = new InvocationHandler() {
+ * &#64;Override
+ * public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ * if (method.getName().equals("nextInt")) {
+ * // Chosen by fair dice roll, guaranteed to be random.
+ * return 4;
+ * }
+ * Object result = ProxyBuilder.callSuper(proxy, method, args);
+ * System.out.println("Method: " + method.getName() + " args: "
+ * + Arrays.toString(args) + " result: " + result);
+ * return result;
+ * }
+ * };
+ * Random debugRandom = ProxyBuilder.forClass(Random.class)
+ * .dexCache(getInstrumentation().getTargetContext().getDir("dx", Context.MODE_PRIVATE))
+ * .handler(handler)
+ * .build();
+ * assertEquals(4, debugRandom.nextInt());
+ * debugRandom.setSeed(0);
+ * assertTrue(debugRandom.nextBoolean());
+ * </pre>
+ * <h3>Usage</h3>
+ * Call {@link #forClass(Class)} for the Class you wish to proxy. Call
+ * {@link #handler(InvocationHandler)} passing in an {@link InvocationHandler}, and then call
+ * {@link #build()}. The returned instance will be a dynamically generated subclass where all method
+ * calls will be delegated to the invocation handler, except as noted below.
+ * <p>
+ * The static method {@link #callSuper(Object, Method, Object...)} allows you to access the original
+ * super method for a given proxy. This allows the invocation handler to selectively override some
+ * methods but not others.
+ * <p>
+ * By default, the {@link #build()} method will call the no-arg constructor belonging to the class
+ * being proxied. If you wish to call a different constructor, you must provide arguments for both
+ * {@link #constructorArgTypes(Class[])} and {@link #constructorArgValues(Object[])}.
+ * <p>
+ * This process works only for classes with public and protected level of visibility.
+ * <p>
+ * You may proxy abstract classes. You may not proxy final classes.
+ * <p>
+ * Only non-private, non-final, non-static methods will be dispatched to the invocation handler.
+ * Private, static or final methods will always call through to the superclass as normal.
+ * <p>
+ * The {@link #finalize()} method on {@code Object} will not be proxied.
+ * <p>
+ * You must provide a dex cache directory via the {@link #dexCache(File)} method. You should take
+ * care not to make this a world-writable directory, so that third parties cannot inject code into
+ * your application. A suitable parameter for these output directories would be something like
+ * this:
+ * <pre>{@code
+ * getApplicationContext().getDir("dx", Context.MODE_PRIVATE);
+ * }</pre>
+ * <p>
+ * If the base class to be proxied leaks the {@code this} pointer in the constructor (bad practice),
+ * that is to say calls a non-private non-final method from the constructor, the invocation handler
+ * will not be invoked. As a simple concrete example, when proxying Random we discover that it
+ * inernally calls setSeed during the constructor. The proxy will not intercept this call during
+ * proxy construction, but will intercept as normal afterwards. This behaviour may be subject to
+ * change in future releases.
+ * <p>
+ * This class is <b>not thread safe</b>.
+ */
+public final class ProxyBuilder<T> {
+ private static final String FIELD_NAME_HANDLER = "$__handler";
+ private static final String FIELD_NAME_METHODS = "$__methodArray";
+
+ private final Class<T> baseClass;
+ private ClassLoader parentClassLoader = ProxyBuilder.class.getClassLoader();
+ private InvocationHandler handler;
+ private File dexCache;
+ private Class<?>[] constructorArgTypes = new Class[0];
+ private Object[] constructorArgValues = new Object[0];
+
+ private ProxyBuilder(Class<T> clazz) {
+ baseClass = clazz;
+ }
+
+ public static <T> ProxyBuilder<T> forClass(Class<T> clazz) {
+ return new ProxyBuilder<T>(clazz);
+ }
+
+ /**
+ * Specifies the parent ClassLoader to use when creating the proxy.
+ *
+ * <p>If null, {@code ProxyBuilder.class.getClassLoader()} will be used.
+ */
+ public ProxyBuilder<T> parentClassLoader(ClassLoader parent) {
+ parentClassLoader = parent;
+ return this;
+ }
+
+ public ProxyBuilder<T> handler(InvocationHandler handler) {
+ this.handler = handler;
+ return this;
+ }
+
+ public ProxyBuilder<T> dexCache(File dexCache) {
+ this.dexCache = dexCache;
+ return this;
+ }
+
+ public ProxyBuilder<T> constructorArgValues(Object... constructorArgValues) {
+ this.constructorArgValues = constructorArgValues;
+ return this;
+ }
+
+ public ProxyBuilder<T> constructorArgTypes(Class<?>... constructorArgTypes) {
+ this.constructorArgTypes = constructorArgTypes;
+ return this;
+ }
+
+ /**
+ * Create a new instance of the class to proxy.
+ *
+ * @throws UnsupportedOperationException if the class we are trying to create a proxy for is
+ * not accessible.
+ * @throws DexCacheException if an exception occurred writing to the {@code dexCache} directory.
+ * @throws UndeclaredThrowableException if the constructor for the base class to proxy throws
+ * a declared exception during construction.
+ * @throws IllegalArgumentException if the handler is null, if the constructor argument types
+ * do not match the constructor argument values, or if no such constructor exists.
+ */
+ public T build() {
+ check(handler != null, "handler == null");
+ check(constructorArgTypes.length == constructorArgValues.length,
+ "constructorArgValues.length != constructorArgTypes.length");
+ DexGenerator generator = new DexGenerator();
+ String generatedName = getMethodNameForProxyOf(baseClass);
+ Type<? extends T> generatedType = Type.get("L" + generatedName + ";");
+ Type<T> superType = Type.get(baseClass);
+ generateConstructorsAndFields(generator, generatedType, superType, baseClass);
+ Method[] methodsToProxy = getMethodsToProxy(baseClass);
+ generateCodeForAllMethods(generator, generatedType, methodsToProxy, superType);
+ generator.declare(generatedType, generatedName + ".generated", ACC_PUBLIC, superType);
+ ClassLoader classLoader;
+ try {
+ classLoader = generator.load(parentClassLoader, dexCache, dexCache);
+ } catch (IOException e) {
+ throw new DexCacheException(e);
+ }
+ Class<? extends T> proxyClass;
+ try {
+ proxyClass = loadClass(classLoader, generatedName);
+ } catch (IllegalAccessError e) {
+ // Thrown when the base class is not accessible.
+ throw new UnsupportedOperationException("cannot proxy inaccessible classes", e);
+ } catch (ClassNotFoundException e) {
+ // Should not be thrown, we're sure to have generated this class.
+ throw new AssertionError(e);
+ }
+ setMethodsStaticField(proxyClass, methodsToProxy);
+ Constructor<? extends T> constructor;
+ try {
+ constructor = proxyClass.getConstructor(constructorArgTypes);
+ } catch (NoSuchMethodException e) {
+ // Thrown when the ctor to be called does not exist.
+ throw new IllegalArgumentException("could not find matching constructor", e);
+ }
+ T result;
+ try {
+ result = constructor.newInstance(constructorArgValues);
+ } catch (InstantiationException e) {
+ // Should not be thrown, generated class is not abstract.
+ throw new AssertionError(e);
+ } catch (IllegalAccessException e) {
+ // Should not be thrown, the generated constructor is accessible.
+ throw new AssertionError(e);
+ } catch (InvocationTargetException e) {
+ // Thrown when the base class ctor throws an exception.
+ throw launderCause(e);
+ }
+ setHandlerInstanceField(result, handler);
+ return result;
+ }
+
+ // The type cast is safe: the generated type will extend the base class type.
+ @SuppressWarnings("unchecked")
+ private Class<? extends T> loadClass(ClassLoader classLoader, String generatedName)
+ throws ClassNotFoundException {
+ return (Class<? extends T>) classLoader.loadClass(generatedName);
+ }
+
+ private static RuntimeException launderCause(InvocationTargetException e) {
+ Throwable cause = e.getCause();
+ // Errors should be thrown as they are.
+ if (cause instanceof Error) {
+ throw (Error) cause;
+ }
+ // RuntimeException can be thrown as-is.
+ if (cause instanceof RuntimeException) {
+ throw (RuntimeException) cause;
+ }
+ // Declared exceptions will have to be wrapped.
+ throw new UndeclaredThrowableException(cause);
+ }
+
+ private static void setHandlerInstanceField(Object instance, InvocationHandler handler) {
+ try {
+ Field handlerField = instance.getClass().getDeclaredField(FIELD_NAME_HANDLER);
+ handlerField.setAccessible(true);
+ handlerField.set(instance, handler);
+ } catch (NoSuchFieldException e) {
+ // Should not be thrown, generated proxy class has been generated with this field.
+ throw new AssertionError(e);
+ } catch (IllegalAccessException e) {
+ // Should not be thrown, we just set the field to accessible.
+ throw new AssertionError(e);
+ }
+ }
+
+ private static void setMethodsStaticField(Class<?> proxyClass, Method[] methodsToProxy) {
+ try {
+ Field methodArrayField = proxyClass.getDeclaredField(FIELD_NAME_METHODS);
+ methodArrayField.setAccessible(true);
+ methodArrayField.set(null, methodsToProxy);
+ } catch (NoSuchFieldException e) {
+ // Should not be thrown, generated proxy class has been generated with this field.
+ throw new AssertionError(e);
+ } catch (IllegalAccessException e) {
+ // Should not be thrown, we just set the field to accessible.
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Returns the proxy's {@link InvocationHandler}.
+ *
+ * @throws IllegalArgumentException if the object supplied is not a proxy created by this class.
+ */
+ public static InvocationHandler getInvocationHandler(Object instance) {
+ try {
+ Field field = instance.getClass().getDeclaredField(FIELD_NAME_HANDLER);
+ field.setAccessible(true);
+ return (InvocationHandler) field.get(instance);
+ } catch (NoSuchFieldException e) {
+ throw new IllegalArgumentException("Not a valid proxy instance", e);
+ } catch (IllegalAccessException e) {
+ // Should not be thrown, we just set the field to accessible.
+ throw new AssertionError(e);
+ }
+ }
+
+ private static <T, G extends T> void generateCodeForAllMethods(DexGenerator generator,
+ Type<G> generatedType, Method[] methodsToProxy, Type<T> superclassType) {
+ Type<InvocationHandler> handlerType = Type.get(InvocationHandler.class);
+ Type<Method[]> methodArrayType = Type.get(Method[].class);
+ FieldId<G, InvocationHandler> handlerField =
+ generatedType.getField(handlerType, FIELD_NAME_HANDLER);
+ FieldId<G, Method[]> allMethods =
+ generatedType.getField(methodArrayType, FIELD_NAME_METHODS);
+ Type<Method> methodType = Type.get(Method.class);
+ Type<Object[]> objectArrayType = Type.get(Object[].class);
+ MethodId<InvocationHandler, Object> methodInvoke = handlerType.getMethod(Type.OBJECT,
+ "invoke", Type.OBJECT, methodType, objectArrayType);
+ for (int m = 0; m < methodsToProxy.length; ++m) {
+ /*
+ * If the 5th method on the superclass Example that can be overridden were to look like
+ * this:
+ *
+ * public int doSomething(Bar param0, int param1) {
+ * ...
+ * }
+ *
+ * Then the following code will generate a method on the proxy that looks something
+ * like this:
+ *
+ * public int doSomething(Bar param0, int param1) {
+ * int methodIndex = 4;
+ * Method[] allMethods = Example_Proxy.$__methodArray;
+ * Method thisMethod = allMethods[methodIndex];
+ * int argsLength = 2;
+ * Object[] args = new Object[argsLength];
+ * InvocationHandler localHandler = this.$__handler;
+ * // for-loop begins
+ * int p = 0;
+ * Bar parameter0 = param0;
+ * args[p] = parameter0;
+ * p = 1;
+ * int parameter1 = param1;
+ * Integer boxed1 = Integer.valueOf(parameter1);
+ * args[p] = boxed1;
+ * // for-loop ends
+ * Object result = localHandler.invoke(this, thisMethod, args);
+ * Integer castResult = (Integer) result;
+ * int unboxedResult = castResult.intValue();
+ * return unboxedResult;
+ * }
+ *
+ * Or, in more idiomatic Java:
+ *
+ * public int doSomething(Bar param0, int param1) {
+ * if ($__handler == null) {
+ * return super.doSomething(param0, param1);
+ * }
+ * return __handler.invoke(this, __methodArray[4],
+ * new Object[] { param0, Integer.valueOf(param1) });
+ * }
+ */
+ Method method = methodsToProxy[m];
+ String name = method.getName();
+ Class<?>[] argClasses = method.getParameterTypes();
+ Type<?>[] argTypes = new Type<?>[argClasses.length];
+ for (int i = 0; i < argTypes.length; ++i) {
+ argTypes[i] = Type.get(argClasses[i]);
+ }
+ Class<?> returnType = method.getReturnType();
+ Type<?> resultType = Type.get(returnType);
+ MethodId<T, ?> superMethod = superclassType.getMethod(resultType, name, argTypes);
+ MethodId<?, ?> methodId = generatedType.getMethod(resultType, name, argTypes);
+ Code code = generator.declare(methodId, ACC_PUBLIC);
+ Local<G> localThis = code.getThis(generatedType);
+ Local<InvocationHandler> localHandler = code.newLocal(handlerType);
+ Local<Object> invokeResult = code.newLocal(Type.OBJECT);
+ Local<Integer> intValue = code.newLocal(Type.INT);
+ Local<Object[]> args = code.newLocal(objectArrayType);
+ Local<Integer> argsLength = code.newLocal(Type.INT);
+ Local<Object> temp = code.newLocal(Type.OBJECT);
+ Local<?> resultHolder = code.newLocal(resultType);
+ Local<Method[]> methodArray = code.newLocal(methodArrayType);
+ Local<Method> thisMethod = code.newLocal(methodType);
+ Local<Integer> methodIndex = code.newLocal(Type.INT);
+ Class<?> aBoxedClass = PRIMITIVE_TO_BOXED.get(returnType);
+ Local<?> aBoxedResult = null;
+ if (aBoxedClass != null) {
+ aBoxedResult = code.newLocal(Type.get(aBoxedClass));
+ }
+ Local<?>[] superArgs2 = new Local<?>[argClasses.length];
+ Local<?> superResult2 = code.newLocal(resultType);
+ Local<InvocationHandler> nullHandler = code.newLocal(handlerType);
+
+ code.loadConstant(methodIndex, m);
+ code.sget(allMethods, methodArray);
+ code.aget(methodArray, methodIndex, thisMethod);
+ code.loadConstant(argsLength, argTypes.length);
+ code.newArray(argsLength, args);
+ code.iget(handlerField, localThis, localHandler);
+
+ // if (proxy == null)
+ code.loadConstant(nullHandler, null);
+ Label handlerNullCase = code.newLabel();
+ code.compare(Comparison.EQ, nullHandler, localHandler, handlerNullCase);
+
+ // This code is what we execute when we have a valid proxy: delegate to invocation
+ // handler.
+ for (int p = 0; p < argTypes.length; ++p) {
+ code.loadConstant(intValue, p);
+ Local<?> parameter = code.getParameter(p, argTypes[p]);
+ Local<?> unboxedIfNecessary = boxIfRequired(generator, code, parameter, temp);
+ code.aput(args, intValue, unboxedIfNecessary);
+ }
+ code.invokeInterface(methodInvoke, invokeResult, localHandler,
+ localThis, thisMethod, args);
+ generateCodeForReturnStatement(code, returnType, invokeResult, resultHolder,
+ aBoxedResult);
+
+ // This code is executed if proxy is null: call the original super method.
+ // This is required to handle the case of construction of an object which leaks the
+ // "this" pointer.
+ code.mark(handlerNullCase);
+ for (int i = 0; i < superArgs2.length; ++i) {
+ superArgs2[i] = code.getParameter(i, argTypes[i]);
+ }
+ if (void.class.equals(returnType)) {
+ code.invokeSuper(superMethod, null, localThis, superArgs2);
+ code.returnVoid();
+ } else {
+ invokeSuper(superMethod, code, localThis, superArgs2, superResult2);
+ code.returnValue(superResult2);
+ }
+
+ /*
+ * And to allow calling the original super method, the following is also generated:
+ *
+ * public int super_doSomething(Bar param0, int param1) {
+ * int result = super.doSomething(param0, param1);
+ * return result;
+ * }
+ */
+ String superName = "super_" + name;
+ MethodId<G, ?> callsSuperMethod = generatedType.getMethod(
+ resultType, superName, argTypes);
+ Code superCode = generator.declare(callsSuperMethod, ACC_PUBLIC);
+ Local<G> superThis = superCode.getThis(generatedType);
+ Local<?>[] superArgs = new Local<?>[argClasses.length];
+ for (int i = 0; i < superArgs.length; ++i) {
+ superArgs[i] = superCode.getParameter(i, argTypes[i]);
+ }
+ if (void.class.equals(returnType)) {
+ superCode.invokeSuper(superMethod, null, superThis, superArgs);
+ superCode.returnVoid();
+ } else {
+ Local<?> superResult = superCode.newLocal(resultType);
+ invokeSuper(superMethod, superCode, superThis, superArgs, superResult);
+ superCode.returnValue(superResult);
+ }
+ }
+ }
+
+ // This one is tricky to fix, I gave up.
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ private static <T> void invokeSuper(MethodId superMethod, Code superCode,
+ Local superThis, Local[] superArgs, Local superResult) {
+ superCode.invokeSuper(superMethod, superResult, superThis, superArgs);
+ }
+
+ private static Local<?> boxIfRequired(DexGenerator generator, Code code, Local<?> parameter,
+ Local<Object> temp) {
+ MethodId<?, ?> unboxMethod = PRIMITIVE_TYPE_TO_UNBOX_METHOD.get(parameter.getType());
+ if (unboxMethod == null) {
+ return parameter;
+ }
+ code.invokeStatic(unboxMethod, temp, parameter);
+ return temp;
+ }
+
+ public static Object callSuper(Object proxy, Method method, Object... args)
+ throws SecurityException, IllegalAccessException,
+ InvocationTargetException, NoSuchMethodException {
+ return proxy.getClass()
+ .getMethod("super_" + method.getName(), method.getParameterTypes())
+ .invoke(proxy, args);
+ }
+
+ private static void check(boolean condition, String message) {
+ if (!condition) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ private static <T, G extends T> void generateConstructorsAndFields(DexGenerator generator,
+ Type<G> generatedType, Type<T> superType, Class<T> superClass) {
+ Type<InvocationHandler> handlerType = Type.get(InvocationHandler.class);
+ Type<Method[]> methodArrayType = Type.get(Method[].class);
+ FieldId<G, InvocationHandler> handlerField = generatedType.getField(
+ handlerType, FIELD_NAME_HANDLER);
+ generator.declare(handlerField, ACC_PRIVATE, null);
+ FieldId<G, Method[]> allMethods = generatedType.getField(
+ methodArrayType, FIELD_NAME_METHODS);
+ generator.declare(allMethods, ACC_PRIVATE | ACC_STATIC, null);
+ for (Constructor<T> constructor : getConstructorsToOverwrite(superClass)) {
+ if (constructor.getModifiers() == Modifier.FINAL) {
+ continue;
+ }
+ Type<?>[] types = classArrayToTypeArray(constructor.getParameterTypes());
+ MethodId<?, ?> method = generatedType.getConstructor(types);
+ Code constructorCode = generator.declare(method, ACC_PUBLIC | ACC_CONSTRUCTOR);
+ Local<G> thisRef = constructorCode.getThis(generatedType);
+ Local<?>[] params = new Local[types.length];
+ for (int i = 0; i < params.length; ++i) {
+ params[i] = constructorCode.getParameter(i, types[i]);
+ }
+ MethodId<T, ?> superConstructor = superType.getConstructor(types);
+ constructorCode.invokeDirect(superConstructor, null, thisRef, params);
+ constructorCode.returnVoid();
+ }
+ }
+
+ // The type parameter on Constructor is the class in which the constructor is declared.
+ // The getDeclaredConstructors() method gets constructors declared only in the given class,
+ // hence this cast is safe.
+ @SuppressWarnings("unchecked")
+ private static <T> Constructor<T>[] getConstructorsToOverwrite(Class<T> clazz) {
+ return (Constructor<T>[]) clazz.getDeclaredConstructors();
+ }
+
+ /**
+ * Gets all {@link Method} objects we can proxy in the hierarchy of the supplied class.
+ */
+ private static <T> Method[] getMethodsToProxy(Class<T> clazz) {
+ Set<MethodSetEntry> methodsToProxy = new HashSet<MethodSetEntry>();
+ for (Class<?> current = clazz; current != null; current = current.getSuperclass()) {
+ for (Method method : current.getDeclaredMethods()) {
+ if ((method.getModifiers() & Modifier.FINAL) != 0) {
+ // Skip final methods, we can't override them.
+ continue;
+ }
+ if ((method.getModifiers() & Modifier.STATIC) != 0) {
+ // Skip static methods, overriding them has no effect.
+ continue;
+ }
+ if (method.getName().equals("finalize") && method.getParameterTypes().length == 0) {
+ // Skip finalize method, it's likely important that it execute as normal.
+ continue;
+ }
+ methodsToProxy.add(new MethodSetEntry(method));
+ }
+ }
+ Method[] results = new Method[methodsToProxy.size()];
+ int i = 0;
+ for (MethodSetEntry entry : methodsToProxy) {
+ results[i++] = entry.originalMethod;
+ }
+ return results;
+ }
+
+ private static <T> String getMethodNameForProxyOf(Class<T> clazz) {
+ return clazz.getSimpleName() + "_Proxy";
+ }
+
+ private static Type<?>[] classArrayToTypeArray(Class<?>[] input) {
+ Type<?>[] result = new Type[input.length];
+ for (int i = 0; i < input.length; ++i) {
+ result[i] = Type.get(input[i]);
+ }
+ return result;
+ }
+
+ /**
+ * Calculates the correct return statement code for a method.
+ * <p>
+ * A void method will not return anything. A method that returns a primitive will need to
+ * unbox the boxed result. Otherwise we will cast the result.
+ */
+ // This one is tricky to fix, I gave up.
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ private static void generateCodeForReturnStatement(Code code, Class methodReturnType,
+ Local localForResultOfInvoke, Local localOfMethodReturnType, Local aBoxedResult) {
+ if (PRIMITIVE_TO_UNBOX_METHOD.containsKey(methodReturnType)) {
+ code.typeCast(localForResultOfInvoke, aBoxedResult);
+ MethodId unboxingMethodFor = getUnboxMethodForPrimitive(methodReturnType);
+ code.invokeVirtual(unboxingMethodFor, localOfMethodReturnType, aBoxedResult);
+ code.returnValue(localOfMethodReturnType);
+ } else if (void.class.equals(methodReturnType)) {
+ code.returnVoid();
+ } else {
+ code.typeCast(localForResultOfInvoke, localOfMethodReturnType);
+ code.returnValue(localOfMethodReturnType);
+ }
+ }
+
+ private static MethodId<?, ?> getUnboxMethodForPrimitive(Class<?> methodReturnType) {
+ return PRIMITIVE_TO_UNBOX_METHOD.get(methodReturnType);
+ }
+
+ private static final Map<Class<?>, Class<?>> PRIMITIVE_TO_BOXED;
+ static {
+ PRIMITIVE_TO_BOXED = new HashMap<Class<?>, Class<?>>();
+ PRIMITIVE_TO_BOXED.put(boolean.class, Boolean.class);
+ PRIMITIVE_TO_BOXED.put(int.class, Integer.class);
+ PRIMITIVE_TO_BOXED.put(byte.class, Byte.class);
+ PRIMITIVE_TO_BOXED.put(long.class, Long.class);
+ PRIMITIVE_TO_BOXED.put(short.class, Short.class);
+ PRIMITIVE_TO_BOXED.put(float.class, Float.class);
+ PRIMITIVE_TO_BOXED.put(double.class, Double.class);
+ PRIMITIVE_TO_BOXED.put(char.class, Character.class);
+ }
+
+ private static final Map<Type<?>, MethodId<?, ?>> PRIMITIVE_TYPE_TO_UNBOX_METHOD;
+ static {
+ PRIMITIVE_TYPE_TO_UNBOX_METHOD = new HashMap<Type<?>, MethodId<?, ?>>();
+ for (Map.Entry<Class<?>, Class<?>> entry : PRIMITIVE_TO_BOXED.entrySet()) {
+ Type<?> primitiveType = Type.get(entry.getKey());
+ Type<?> boxedType = Type.get(entry.getValue());
+ MethodId<?, ?> valueOfMethod = boxedType.getMethod(boxedType, "valueOf", primitiveType);
+ PRIMITIVE_TYPE_TO_UNBOX_METHOD.put(primitiveType, valueOfMethod);
+ }
+ }
+
+ /**
+ * Map from primitive type to method used to unbox a boxed version of the primitive.
+ * <p>
+ * This is required for methods whose return type is primitive, since the
+ * {@link InvocationHandler} will return us a boxed result, and we'll need to convert it back to
+ * primitive value.
+ */
+ private static final Map<Class<?>, MethodId<?, ?>> PRIMITIVE_TO_UNBOX_METHOD;
+ static {
+ Map<Class<?>, MethodId<?, ?>> map = new HashMap<Class<?>, MethodId<?, ?>>();
+ map.put(boolean.class, Type.get(Boolean.class).getMethod(Type.BOOLEAN, "booleanValue"));
+ map.put(int.class, Type.get(Integer.class).getMethod(Type.INT, "intValue"));
+ map.put(byte.class, Type.get(Byte.class).getMethod(Type.BYTE, "byteValue"));
+ map.put(long.class, Type.get(Long.class).getMethod(Type.LONG, "longValue"));
+ map.put(short.class, Type.get(Short.class).getMethod(Type.SHORT, "shortValue"));
+ map.put(float.class, Type.get(Float.class).getMethod(Type.FLOAT, "floatValue"));
+ map.put(double.class, Type.get(Double.class).getMethod(Type.DOUBLE, "doubleValue"));
+ map.put(char.class, Type.get(Character.class).getMethod(Type.CHAR, "charValue"));
+ PRIMITIVE_TO_UNBOX_METHOD = map;
+ }
+
+ /**
+ * Wrapper class to let us disambiguate {@link Method} objects.
+ * <p>
+ * The purpose of this class is to override the {@link #equals(Object)} and {@link #hashCode()}
+ * methods so we can use a {@link Set} to remove duplicate methods that are overrides of one
+ * another. For these purposes, we consider two methods to be equal if they have the same
+ * name, return type, and parameter types.
+ */
+ private static class MethodSetEntry {
+ private final String name;
+ private final Class<?>[] paramTypes;
+ private final Class<?> returnType;
+ private final Method originalMethod;
+
+ public MethodSetEntry(Method method) {
+ originalMethod = method;
+ name = method.getName();
+ paramTypes = method.getParameterTypes();
+ returnType = method.getReturnType();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof MethodSetEntry) {
+ MethodSetEntry other = (MethodSetEntry) o;
+ return name.equals(other.name)
+ && returnType.equals(other.returnType)
+ && Arrays.equals(paramTypes, other.paramTypes);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result += 31 * result + name.hashCode();
+ result += 31 * result + returnType.hashCode();
+ result += 31 * result + Arrays.hashCode(paramTypes);
+ return result;
+ }
+ }
+}