/* * Copyright (c) 2011, 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. */ /* * @test * @summary Proxy Generator Combo tests * @library /test/langtools/tools/javac/lib . * @modules jdk.compiler/com.sun.tools.javac.api * jdk.compiler/com.sun.tools.javac.code * jdk.compiler/com.sun.tools.javac.comp * jdk.compiler/com.sun.tools.javac.file * jdk.compiler/com.sun.tools.javac.main * jdk.compiler/com.sun.tools.javac.tree * jdk.compiler/com.sun.tools.javac.util * @build combo.ComboTestHelper * @run main/othervm ProxyGeneratorCombo */ import java.io.IOException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; import java.lang.reflect.UndeclaredThrowableException; import java.util.Arrays; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.StringJoiner; import combo.ComboInstance; import combo.ComboParameter; import combo.ComboTask.Result; import combo.ComboTestHelper; public class ProxyGeneratorCombo extends ComboInstance { // The unique number to qualify interface names, unique across multiple runs private static int uniqueId = 0; /** * Class Access kinds. */ enum ClassAccessKind implements ComboParameter { PUBLIC("public"), PACKAGE(""), ; String classAccessTemplate; ClassAccessKind(String classAccessTemplate) { this.classAccessTemplate = classAccessTemplate; } @Override public String expand(String optParameter) { return classAccessTemplate; } } /** * Signatures of methods to be tested. */ enum MethodsKind implements ComboParameter { NONE(""), ZERO("#{METHODACCESS} void zero() #{EXCEPTION};"), ONE("#{METHODACCESS} void one(#{ARG[0]} a) #{EXCEPTION};"), TWO("#{METHODACCESS} void one(#{ARG[0]} b);\n" + "#{METHODACCESS} void two(#{ARG[0]} a, #{ARG[1]} b);"), THREE("#{METHODACCESS} void one(#{ARG[0]} a);\n" + "#{METHODACCESS} void two(#{ARG[0]} a, #{ARG[1]} b);\n" + "#{METHODACCESS} void three(#{ARG[0]} a, #{ARG[1]} b, #{ARG[0]} c);"); String methodsKindTemplate; MethodsKind(String methodsKindTemplate) { this.methodsKindTemplate = methodsKindTemplate; } @Override public String expand(String optParameter) { return methodsKindTemplate; } } /** * Type of arguments to insert in method signatures */ enum ArgumentKind implements ComboParameter { BOOLEAN("boolean"), BYTE("byte"), CHAR("char"), SHORT("short"), INT("int"), LONG("long"), FLOAT("float"), DOUBLE("double"), STRING("String"); String argumentsKindTemplate; ArgumentKind(String argumentsKindTemplate) { this.argumentsKindTemplate = argumentsKindTemplate; } @Override public String expand(String optParameter) { return argumentsKindTemplate; } } /** * Exceptions to be added to zero and one methods. */ enum ExceptionKind implements ComboParameter { NONE(null), EXCEPTION(java.lang.Exception.class), RUNTIME_EXCEPTION(java.lang.RuntimeException.class), ILLEGAL_ARGUMENT_EXCEPTION(java.lang.IllegalArgumentException.class), IOEXCEPTION(java.io.IOException.class), /** * Used only for throw testing, is empty for throws clause in the source, */ UNDECLARED_EXCEPTION(Exception1.class), ; Class exceptionKindClass; ExceptionKind(Class exceptionKindClass) { this.exceptionKindClass = exceptionKindClass; } @Override public String expand(String optParameter) { return exceptionKindClass == null || exceptionKindClass == Exception1.class ? "" : "throws " + exceptionKindClass.getName(); } } /** * Extra interfaces to be added. */ enum MultiInterfacesKind implements ComboParameter { NONE(new Class[0]), INTERFACE_WITH_EXCEPTION(new Class[] {InterfaceWithException.class}), ; Class[] multiInterfaceClasses; MultiInterfacesKind(Class[] multiInterfaceClasses) { this.multiInterfaceClasses = multiInterfaceClasses; } @Override // Not used for expansion only execution public String expand(String optParameter) { throw new RuntimeException("NYI"); } Class[] classes() { return multiInterfaceClasses; } } @Override public int id() { return ++uniqueId; } protected void fail(String msg, Throwable thrown) { super.fail(msg); thrown.printStackTrace(); } /** * Test interface with a "one(int)" method. */ interface InterfaceWithException { // The signature must match the ONE MethodsKind above void one(int a) throws RuntimeException, IOException; } /** * Main to generate combinations and run the tests. * @param args unused * @throws Exception In case of failure */ public static void main(String... args) throws Exception { // Test variations of access declarations new ComboTestHelper() .withDimension("CLASSACCESS", ClassAccessKind.values()) .withDimension("METHODACCESS", new ClassAccessKind[]{ClassAccessKind.PUBLIC}) .withDimension("METHODS", ProxyGeneratorCombo::saveMethod, new MethodsKind[] {MethodsKind.NONE, MethodsKind.ZERO, MethodsKind.ONE}) .withDimension("ARG[0]", new ArgumentKind[] {ArgumentKind.INT}) .withDimension("EXCEPTION", ProxyGeneratorCombo::saveException, new ExceptionKind[]{ExceptionKind.NONE}) .run(ProxyGeneratorCombo::new); // Test variations of argument types new ComboTestHelper() .withDimension("CLASSACCESS", new ClassAccessKind[]{ClassAccessKind.PUBLIC}) .withDimension("METHODACCESS", new ClassAccessKind[]{ClassAccessKind.PUBLIC}) .withDimension("METHODS", ProxyGeneratorCombo::saveMethod, MethodsKind.values()) .withArrayDimension("ARG", ProxyGeneratorCombo::saveArg, 2, ArgumentKind.values()) .withDimension("EXCEPTION", ProxyGeneratorCombo::saveException, new ExceptionKind[]{ExceptionKind.NONE}) .withFilter(ProxyGeneratorCombo::filter) .run(ProxyGeneratorCombo::new); // Test for conflicts in Exceptions on methods with the same signatures new ComboTestHelper() .withDimension("CLASSACCESS", new ClassAccessKind[]{ClassAccessKind.PUBLIC}) .withDimension("METHODACCESS", new ClassAccessKind[]{ClassAccessKind.PUBLIC}) .withDimension("METHODS", ProxyGeneratorCombo::saveMethod, new MethodsKind[] { MethodsKind.ZERO}) .withDimension("EXCEPTION", ProxyGeneratorCombo::saveException, ExceptionKind.values()) .withDimension("MULTI_INTERFACES", ProxyGeneratorCombo::saveInterface, new MultiInterfacesKind[] {MultiInterfacesKind.NONE}) .run(ProxyGeneratorCombo::new); } /** * Basic template. */ String template = "#{CLASSACCESS} interface #{TESTNAME} {\n" + "#{METHODS}" + "}"; // Saved values of Combo values private MultiInterfacesKind currInterface = MultiInterfacesKind.NONE; private MethodsKind currMethod = MethodsKind.NONE; private ExceptionKind currException = ExceptionKind.NONE; private ArgumentKind[] currArgs = new ArgumentKind[0]; void saveInterface(ComboParameter s) { currInterface = (MultiInterfacesKind)s; } void saveMethod(ComboParameter s) { currMethod = (MethodsKind)s; } void saveException(ComboParameter s) { currException = (ExceptionKind)s; } void saveArg(ComboParameter s, int index) { if (index >= currArgs.length) { currArgs = Arrays.copyOf(currArgs, index + 1); } currArgs[index] = (ArgumentKind)s; } /** * Filter out needless tests (mostly with more variations of arguments than needed). * @return true to run the test, false if not */ boolean filter() { if ((currMethod == MethodsKind.NONE || currMethod == MethodsKind.ZERO) && currArgs.length >= 2) { return currArgs[0] == ArgumentKind.INT && currArgs[1] == ArgumentKind.INT; } if (currMethod == MethodsKind.ONE && currArgs.length >= 2 ) { return currArgs[0] == currArgs[1]; } return true; } /** * Generate the source file and compile. * Generate a proxy for the interface and test the resulting Proxy * for the methods, exceptions and handling of a thrown exception * @throws IOException catch all IOException */ @Override public void doWork() throws IOException { String cp = System.getProperty("test.classes"); String ifaceName = "Interface_" + this.id(); newCompilationTask() .withSourceFromTemplate(ifaceName, template.replace("#{TESTNAME}", ifaceName)) .withOption("-d") .withOption(cp) .generate(this::checkCompile); try { ClassLoader loader = ClassLoader.getSystemClassLoader(); Class tc = Class.forName(ifaceName); InvocationHandler handler = new ProxyHandler(currException.exceptionKindClass); // Construct array of interfaces for the proxy Class[] interfaces = new Class[currInterface.classes().length + 1]; interfaces[0] = tc; System.arraycopy(currInterface.classes(), 0, interfaces, 1, currInterface.classes().length); Object proxy = Proxy.newProxyInstance(loader, interfaces, handler); if (!Proxy.isProxyClass(proxy.getClass())) { fail("generated proxy is not a proxy class"); return; } for (Class i : interfaces) { if (!i.isAssignableFrom(proxy.getClass())) { fail("proxy is not assignable to " + i.getName()); } } try { String s = proxy.toString(); } catch (Exception ex) { ex.printStackTrace(); fail("proxy.toString() threw an exception"); } checkDeclaredProxyExceptions(proxy, interfaces); if (currMethod == MethodsKind.ZERO && currException != ExceptionKind.NONE) { checkThrowsException(proxy, interfaces); } } catch (Exception ex) { throw new RuntimeException("doWork unexpected", ex); } } /** * Check that the exceptions declared on the proxy match the declarations for * exceptions from the interfaces. * * @param proxy a proxy object * @param interfaces the interfaces that defined it */ void checkDeclaredProxyExceptions(Object proxy, Class[] interfaces) { var allMethods = allMethods(Arrays.asList(interfaces)); Method[] methods = proxy.getClass().getDeclaredMethods(); for (Method m : methods) { String sig = toShortSignature(m); var imethods = allMethods.get(sig); if (imethods != null) { var expectedEx = Set.copyOf(Arrays.asList(m.getExceptionTypes())); var exs = Set.copyOf(extractExceptions(imethods)); if (!expectedEx.equals(exs)) { System.out.printf("mismatch on exceptions for method %s:%nExpected: " + "%s%nActual: %s%n", sig, expectedEx, exs); fail("Exceptions declared on proxy don't match interface methods"); } } } } void checkThrowsException(Object proxy, Class[] interfaces) { ProxyHandler ph = (ProxyHandler)(Proxy.getInvocationHandler(proxy)); try { Method m = proxy.getClass().getDeclaredMethod("zero"); m.invoke(proxy); fail("Missing exception: " + ph.exceptionClass); } catch (NoSuchMethodException nsme) { System.out.printf("No method 'zero()' to test exceptions with%n"); for (var cl : interfaces) { System.out.printf(" i/f %s: %s%n", cl, Arrays.toString(cl.getMethods())); } Method[] methods = proxy.getClass().getMethods(); System.out.printf(" Proxy methods: %s%n", Arrays.toString(methods)); fail("No such method test bug", nsme); } catch (InvocationTargetException actual) { ph.checkThrownException(actual.getTargetException()); } catch (IllegalAccessException iae) { fail("IllegalAccessException", iae); } } /** * Exceptions known to be supported by all methods with the same signature. * @return a list of universal exception types */ private static List> extractExceptions(List methods) { // for all methods with the same signature // start with the exceptions from the first method // while there are any exceptions remaining // look at the next method List> exceptions = null; for (Method m : methods) { var e = m.getExceptionTypes(); if (e.length == 0) return emptyClassList(); List> elist = Arrays.asList(e); if (exceptions == null) { exceptions = elist; // initialize to first method exceptions } else { // for each exception // if it is compatible (both ways) with any of the existing exceptions continue // else remove the current exception var okExceptions = new HashSet>(); for (int j = 0; j < exceptions.size(); j++) { var ex = exceptions.get(j); for (int i = 0; i < elist.size();i++) { var ci = elist.get(i); if (ci.isAssignableFrom(ex)) { okExceptions.add(ex); } if (ex.isAssignableFrom(ci)) { okExceptions.add(ci); } } } if (exceptions.isEmpty()) { // The empty set terminates the search for a common set of exceptions return emptyClassList(); } // Use the new set for the next iteration exceptions = List.copyOf(okExceptions); } } return (exceptions == null) ? emptyClassList() : exceptions; } /** * An empty correctly typed list of classes. * @return An empty typed list of classes */ @SuppressWarnings("unchecked") static List> emptyClassList() { return Collections.EMPTY_LIST; } /** * Accumulate all of the unique methods. * * @param interfaces a list of interfaces * @return a map from signature to List of methods, unique by signature */ private static Map> allMethods(List> interfaces) { Map> methods = new HashMap<>(); for (Class c : interfaces) { for (Method m : c.getMethods()) { if (!Modifier.isStatic(m.getModifiers())) { String sig = toShortSignature(m); methods.computeIfAbsent(sig, s -> new ArrayList()) .add(m); } } } return methods; } /** * The signature of a method without the return type. * @param m a Method * @return the signature with method name and parameters */ static String toShortSignature(Method m) { StringJoiner sj = new StringJoiner(",", m.getName() + "(", ")"); for (Class parameterType : m.getParameterTypes()) { sj.add(parameterType.getTypeName()); } return sj.toString(); } /** * Report any compilation errors. * @param res the result */ void checkCompile(Result res) { if (res.hasErrors()) { fail("invalid diagnostics for source:\n" + res.compilationInfo() + "\nFound error: " + res.hasErrors()); } } /** * The Handler for the proxy includes the method to invoke the proxy * and the expected exception, if any. */ class ProxyHandler implements InvocationHandler { private final Class exceptionClass; ProxyHandler(Class throwable) { this.exceptionClass = throwable; } /** * Invoke a method on the proxy or return a value. * @param proxy the proxy instance that the method was invoked on * @param method a method * @param args some args * @return * @throws Throwable a throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("toString")) { return "Proxy" + System.identityHashCode(proxy); } if (method.getName().equals("zero")) { if (exceptionClass != null) { throw exceptionClass.getDeclaredConstructor().newInstance(); } } return "meth: " + method.toString(); } /** * Check that the expected exception was thrown. * Special case is handled for Exception1 which does not appear in the * throws clause of the method so UndeclaredThrowableException is expected. */ void checkThrownException(Throwable thrown) { if (exceptionClass == Exception1.class && thrown instanceof UndeclaredThrowableException && ((UndeclaredThrowableException)thrown).getUndeclaredThrowable() instanceof Exception1) { // Exception1 caused UndeclaredThrowableException return; } else if (exceptionClass == Exception1.class) { fail("UndeclaredThrowableException", thrown); } if (exceptionClass != null && !exceptionClass.equals(thrown.getClass())) { throw new RuntimeException("Wrong exception thrown: expected: " + exceptionClass + ", actual: " + thrown.getClass()); } } } /** * Exception to be thrown as a test of InvocationTarget. */ static class Exception1 extends Exception { private static final long serialVersionUID = 1L; Exception1() {} } }