/* * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ import java.io.IOException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import static org.testng.Assert.*; /** * @test * @bug 8159746 * @run testng DefaultMethods * @summary Basic tests for Proxy::invokeSuper default method */ public class DefaultMethods { public interface I1 { default int m() { return 10; } } public interface I2 { default int m() { return 20; } private void privateMethod() { throw new Error("should not reach here"); } } // I3::m inherits from I2:m public interface I3 extends I2 { default int m3(String... s) { return Arrays.stream(s).mapToInt(String::length).sum(); } } public interface I4 extends I1, I2 { default int m() { return 40; } default int mix(int a, String b) { return 0; } } public interface I12 extends I1, I2 { @Override int m(); default int sum(int a, int b) { return a + b; } default Object[] concat(Object first, Object... rest) { Object[] result = new Object[1 + rest.length]; result[0] = first; System.arraycopy(rest, 0, result, 1, rest.length); return result; } } public interface IX { default void doThrow(Throwable exception) throws Throwable { throw exception; } } private static Method findDefaultMethod(Class refc, Method m) { try { assertTrue(refc.isInterface()); Method method = refc.getMethod(m.getName(), m.getParameterTypes()); assertTrue(method.isDefault()); return method; } catch (NoSuchMethodException e) { throw new RuntimeException(e); } } @Test public void test() { ClassLoader loader = DefaultMethods.class.getClassLoader(); Object proxy = Proxy.newProxyInstance(loader, new Class[] { I1.class, I2.class}, (o, method, params) -> { return InvocationHandler.invokeDefault(o, findDefaultMethod(I2.class, method), params); }); I1 i1 = (I1) proxy; assertEquals(i1.m(), 20); } // a default method is declared in one of the proxy interfaces @DataProvider(name = "defaultMethods") private Object[][] defaultMethods() { return new Object[][]{ new Object[]{new Class[]{I1.class, I2.class}, true, 10}, new Object[]{new Class[]{I1.class, I3.class}, true, 10}, new Object[]{new Class[]{I1.class, I12.class}, true, 10}, new Object[]{new Class[]{I2.class, I12.class}, true, 20}, new Object[]{new Class[]{I4.class}, true, 40}, new Object[]{new Class[]{I4.class, I3.class}, true, 40}, new Object[]{new Class[]{I12.class}, false, -1}, new Object[]{new Class[]{I12.class, I1.class, I2.class}, false, -1} }; } @Test(dataProvider = "defaultMethods") public void testDefaultMethod(Class[] intfs, boolean isDefault, int expected) throws Throwable { InvocationHandler ih = (proxy, method, params) -> { System.out.format("invoking %s with parameters: %s%n", method, Arrays.toString(params)); switch (method.getName()) { case "m": assertTrue(method.isDefault() == isDefault); assertTrue(Arrays.stream(proxy.getClass().getInterfaces()) .anyMatch(intf -> method.getDeclaringClass() == intf), Arrays.toString(proxy.getClass().getInterfaces())); if (method.isDefault()) { return InvocationHandler.invokeDefault(proxy, method, params); } else { return -1; } default: throw new UnsupportedOperationException(method.toString()); } }; Object proxy = Proxy.newProxyInstance(DefaultMethods.class.getClassLoader(), intfs, ih); Method m = proxy.getClass().getMethod("m"); int result = (int)m.invoke(proxy); assertEquals(result, expected); } // a default method may be declared in a proxy interface or // inherited from a superinterface of a proxy interface @DataProvider(name = "supers") private Object[][] supers() { return new Object[][]{ // invoke "m" implemented in the first proxy interface // same as the method passed to InvocationHandler::invoke new Object[]{new Class[]{I1.class}, I1.class, 10}, new Object[]{new Class[]{I2.class}, I2.class, 20}, new Object[]{new Class[]{I1.class, I2.class}, I1.class, 10}, // "m" is implemented in I2, an indirect superinterface of I3 new Object[]{new Class[]{I3.class}, I3.class, 20}, // "m" is implemented in I1, I2 and overridden in I4 new Object[]{new Class[]{I4.class}, I4.class, 40}, // invoke "m" implemented in the second proxy interface // different from the method passed to InvocationHandler::invoke new Object[]{new Class[]{I1.class, I2.class}, I2.class, 20}, new Object[]{new Class[]{I1.class, I3.class}, I3.class, 20}, // I2::m is implemented in more than one proxy interface directly or indirectly // I3::m resolves to I2::m (indirect superinterface) // I2 is the superinterface of I4 and I4 overrides m // the proxy class can invoke I4::m and I2::m new Object[]{new Class[]{I3.class, I4.class}, I3.class, 20}, new Object[]{new Class[]{I3.class, I4.class}, I4.class, 40}, new Object[]{new Class[]{I4.class, I3.class}, I3.class, 20}, new Object[]{new Class[]{I4.class, I3.class}, I4.class, 40} }; } @Test(dataProvider = "supers") public void testSuper(Class[] intfs, Class proxyInterface, int expected) throws Throwable { final InvocationHandler ih = (proxy, method, params) -> { switch (method.getName()) { case "m": assertTrue(method.isDefault()); return InvocationHandler.invokeDefault(proxy, findDefaultMethod(proxyInterface, method), params); default: throw new UnsupportedOperationException(method.toString()); } }; ClassLoader loader = proxyInterface.getClassLoader(); Object proxy = Proxy.newProxyInstance(loader, intfs, ih); if (proxyInterface == I1.class) { I1 i1 = (I1) proxy; assertEquals(i1.m(), expected); } else if (proxyInterface == I2.class) { I2 i2 = (I2) proxy; assertEquals(i2.m(), expected); } else if (proxyInterface == I3.class) { I3 i3 = (I3) proxy; assertEquals(i3.m(), expected); } else if (proxyInterface == I4.class) { I4 i4 = (I4) proxy; assertEquals(i4.m(), expected); } else { throw new UnsupportedOperationException(proxyInterface.toString()); } // invoke via InvocationHandler.invokeDefaultMethod directly assertEquals(InvocationHandler.invokeDefault(proxy, proxyInterface.getMethod("m")), expected); } // invoke I12 default methods with parameters and var args @Test public void testI12() throws Throwable { final InvocationHandler ih = (proxy, method, params) -> { System.out.format("invoking %s with parameters: %s%n", method, Arrays.toString(params)); switch (method.getName()) { case "sum": case "concat": assertTrue(method.isDefault()); return InvocationHandler.invokeDefault(proxy, method, params); default: throw new UnsupportedOperationException(method.toString()); } }; ClassLoader loader = DefaultMethods.class.getClassLoader(); I12 i12 = (I12) Proxy.newProxyInstance(loader, new Class[] { I12.class }, ih); assertEquals(i12.sum(1, 2), 3); assertEquals(i12.concat(1, 2, 3, 4), new Object[]{1, 2, 3, 4}); Method m = I12.class.getMethod("concat", Object.class, Object[].class); assertTrue(m.isDefault()); assertEquals(InvocationHandler.invokeDefault(i12, m, 100, new Object[] {"foo", true, "bar"}), new Object[] {100, "foo", true, "bar"}); } // test a no-arg default method with and without arguments passed in the invocation @Test public void testEmptyArgument() throws Throwable { ClassLoader loader = DefaultMethods.class.getClassLoader(); Object proxy = Proxy.newProxyInstance(loader, new Class[]{I4.class}, HANDLER); Method m1 = I4.class.getMethod("m"); assertTrue(m1.getDeclaringClass() == I4.class); assertTrue(m1.isDefault()); InvocationHandler.invokeDefault(proxy, m1); InvocationHandler.invokeDefault(proxy, m1, new Object[0]); Method m2 = I4.class.getMethod("mix", int.class, String.class); assertTrue(m1.getDeclaringClass() == I4.class); assertTrue(m1.isDefault()); InvocationHandler.invokeDefault(proxy, m2, Integer.valueOf(100), "foo"); } @Test public void testVarArgs() throws Throwable { ClassLoader loader = DefaultMethods.class.getClassLoader(); I3 proxy = (I3)Proxy.newProxyInstance(loader, new Class[]{I3.class}, HANDLER); Method m = I3.class.getMethod("m3", String[].class); assertTrue(m.isVarArgs() && m.isDefault()); assertEquals(proxy.m3("a", "b", "cde"), 5); assertEquals(InvocationHandler.invokeDefault(proxy, m, (Object)new String[] { "a", "bc" }), 3); } /* * Invoke I12::m which is an abstract method */ @Test(expectedExceptions = {IllegalArgumentException.class}) public void invokeAbstractMethod() throws Exception { ClassLoader loader = DefaultMethods.class.getClassLoader(); I12 proxy = (I12) Proxy.newProxyInstance(loader, new Class[]{I12.class}, HANDLER); Method method = I12.class.getMethod("m"); assertTrue(method.getDeclaringClass() == I12.class); assertFalse(method.isDefault()); proxy.m(); } /* * Invoke a non proxy (default) method with parameters */ @Test(expectedExceptions = {IllegalArgumentException.class}) public void invokeNonProxyMethod() throws Throwable { ClassLoader loader = DefaultMethods.class.getClassLoader(); I3 proxy = (I3) Proxy.newProxyInstance(loader, new Class[]{I3.class}, HANDLER); Method m = I4.class.getMethod("mix", int.class, String.class); assertTrue(m.isDefault()); InvocationHandler.invokeDefault(proxy, m); } // negative cases @DataProvider(name = "negativeCases") private Object[][] negativeCases() { return new Object[][]{ // I4::m overrides I1::m and I2::m new Object[] { new Class[]{I4.class}, I1.class, "m" }, new Object[] { new Class[]{I4.class}, I2.class, "m" }, // I12::m is not a default method new Object[] { new Class[]{I12.class}, I12.class, "m" }, // non-proxy default method new Object[] { new Class[]{I3.class}, I1.class, "m" }, // not a default method and not a proxy interface new Object[] { new Class[]{I12.class}, DefaultMethods.class, "test" }, new Object[] { new Class[]{I12.class}, Runnable.class, "run" }, // I2::privateMethod is a private method new Object[] { new Class[]{I3.class}, I2.class, "privateMethod" } }; } @Test(dataProvider = "negativeCases", expectedExceptions = {IllegalArgumentException.class}) public void testNegativeCase(Class[] interfaces, Class defc, String name) throws Throwable { ClassLoader loader = DefaultMethods.class.getClassLoader(); Object proxy = Proxy.newProxyInstance(loader, interfaces, HANDLER); try { Method method = defc.getDeclaredMethod(name); InvocationHandler.invokeDefault(proxy, method); } catch (Throwable e) { System.out.format("%s method %s::%s exception thrown: %s%n", Arrays.toString(interfaces), defc.getName(), name, e.getMessage()); throw e; } } @DataProvider(name = "illegalArguments") private Object[][] illegalArguments() { return new Object[][] { new Object[] { new Object[0]}, new Object[] { new Object[] { 100 }}, new Object[] { new Object[] { 100, "foo", 100 }}, new Object[] { new Object[] { 100L, "foo" }}, new Object[] { new Object[] { "foo", 100 }}, new Object[] { new Object[] { null, "foo" }} }; } @Test(dataProvider = "illegalArguments", expectedExceptions = {IllegalArgumentException.class}) public void testIllegalArgument(Object[] args) throws Throwable { ClassLoader loader = DefaultMethods.class.getClassLoader(); I4 proxy = (I4)Proxy.newProxyInstance(loader, new Class[]{I4.class}, HANDLER); Method m = I4.class.getMethod("mix", int.class, String.class); assertTrue(m.isDefault()); if (args.length == 0) { // substitute empty args with null since @DataProvider doesn't allow null array args = null; } InvocationHandler.invokeDefault(proxy, m, args); } @DataProvider(name = "throwables") private Object[][] throwables() { return new Object[][] { new Object[] { new IOException() }, new Object[] { new IllegalArgumentException() }, new Object[] { new ClassCastException() }, new Object[] { new NullPointerException() }, new Object[] { new AssertionError() }, new Object[] { new Throwable() } }; } @Test(dataProvider = "throwables") public void testInvocationException(Throwable exception) throws Throwable { ClassLoader loader = DefaultMethods.class.getClassLoader(); IX proxy = (IX)Proxy.newProxyInstance(loader, new Class[]{IX.class}, HANDLER); Method m = IX.class.getMethod("doThrow", Throwable.class); try { InvocationHandler.invokeDefault(proxy, m, exception); } catch (Throwable e) { assertEquals(e, exception); } } private static final InvocationHandler HANDLER = (proxy, method, params) -> { System.out.format("invoking %s with parameters: %s%n", method, Arrays.toString(params)); return InvocationHandler.invokeDefault(proxy, method, params); }; }