1 /*
   2  * Copyright (c) 2011, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 /*
  25  * @test
  26  * @summary Proxy Generator Combo tests
  27  * @library /test/langtools/tools/javac/lib .
  28  * @modules jdk.compiler/com.sun.tools.javac.api
  29  *          jdk.compiler/com.sun.tools.javac.code
  30  *          jdk.compiler/com.sun.tools.javac.comp
  31  *          jdk.compiler/com.sun.tools.javac.main
  32  *          jdk.compiler/com.sun.tools.javac.tree
  33  *          jdk.compiler/com.sun.tools.javac.util
  34  * @build combo.ComboTestHelper
  35  * @run main/othervm ProxyGeneratorCombo
  36  * @run main/othervm -Djdk.proxy.ProxyGenerator.v49=true ProxyGeneratorCombo
  37  */
  38 
  39 import java.io.IOException;
  40 import java.lang.reflect.InvocationHandler;
  41 import java.lang.reflect.InvocationTargetException;
  42 import java.lang.reflect.Method;
  43 import java.lang.reflect.Modifier;
  44 import java.lang.reflect.Proxy;
  45 import java.lang.reflect.UndeclaredThrowableException;
  46 import java.util.Arrays;
  47 import java.util.ArrayList;
  48 import java.util.Collections;
  49 import java.util.HashMap;
  50 import java.util.HashSet;
  51 import java.util.List;
  52 import java.util.Map;
  53 import java.util.Set;
  54 import java.util.StringJoiner;
  55 
  56 import combo.ComboInstance;
  57 import combo.ComboParameter;
  58 import combo.ComboTask.Result;
  59 import combo.ComboTestHelper;
  60 
  61 public class ProxyGeneratorCombo extends ComboInstance<ProxyGeneratorCombo> {
  62 
  63     // The unique number to qualify interface names, unique across multiple runs
  64     private static int uniqueId = 0;
  65 
  66     /**
  67      * Class Access kinds.
  68      */
  69     enum ClassAccessKind implements ComboParameter {
  70         PUBLIC("public"),
  71         PACKAGE(""),
  72         ;
  73 
  74         String classAccessTemplate;
  75 
  76         ClassAccessKind(String classAccessTemplate) {
  77             this.classAccessTemplate = classAccessTemplate;
  78         }
  79 
  80         @Override
  81         public String expand(String optParameter) {
  82             return classAccessTemplate;
  83         }
  84     }
  85 
  86     /**
  87      * Signatures of methods to be tested.
  88      */
  89     enum MethodsKind implements ComboParameter {
  90         NONE(""),
  91         ZERO("#{METHODACCESS} void zero() #{EXCEPTION};"),
  92         ONE("#{METHODACCESS} void one(#{ARG[0]} a) #{EXCEPTION};"),
  93         TWO("#{METHODACCESS} void one(#{ARG[0]} b);\n" +
  94                 "#{METHODACCESS} void two(#{ARG[0]} a, #{ARG[1]} b);"),
  95         THREE("#{METHODACCESS} void one(#{ARG[0]} a);\n" +
  96                 "#{METHODACCESS} void two(#{ARG[0]} a, #{ARG[1]} b);\n" +
  97                 "#{METHODACCESS} void three(#{ARG[0]} a, #{ARG[1]} b, #{ARG[0]} c);");
  98 
  99         String methodsKindTemplate;
 100 
 101         MethodsKind(String methodsKindTemplate) {
 102             this.methodsKindTemplate = methodsKindTemplate;
 103         }
 104 
 105         @Override
 106         public String expand(String optParameter) {
 107             return methodsKindTemplate;
 108         }
 109     }
 110 
 111     /**
 112      * Type of arguments to insert in method signatures
 113      */
 114     enum ArgumentKind implements ComboParameter {
 115         BOOLEAN("boolean"),
 116         BYTE("byte"),
 117         CHAR("char"),
 118         SHORT("short"),
 119         INT("int"),
 120         LONG("long"),
 121         FLOAT("float"),
 122         DOUBLE("double"),
 123         STRING("String");
 124 
 125         String argumentsKindTemplate;
 126 
 127         ArgumentKind(String argumentsKindTemplate) {
 128             this.argumentsKindTemplate = argumentsKindTemplate;
 129         }
 130 
 131         @Override
 132         public String expand(String optParameter) {
 133             return argumentsKindTemplate;
 134         }
 135     }
 136 
 137     /**
 138      * Exceptions to be added to zero and one methods.
 139      */
 140     enum ExceptionKind implements ComboParameter {
 141         NONE(null),
 142         EXCEPTION(java.lang.Exception.class),
 143         RUNTIME_EXCEPTION(java.lang.RuntimeException.class),
 144         ILLEGAL_ARGUMENT_EXCEPTION(java.lang.IllegalArgumentException.class),
 145         IOEXCEPTION(java.io.IOException.class),
 146         /**
 147          * Used only for throw testing, is empty for throws clause in the source,
 148          */
 149         UNDECLARED_EXCEPTION(Exception1.class),
 150         ;
 151 
 152         Class<? extends Throwable> exceptionKindClass;
 153 
 154         ExceptionKind(Class<? extends Throwable> exceptionKindClass) {
 155             this.exceptionKindClass = exceptionKindClass;
 156         }
 157 
 158         @Override
 159         public String expand(String optParameter) {
 160             return exceptionKindClass == null || exceptionKindClass == Exception1.class
 161                     ? "" : "throws " + exceptionKindClass.getName();
 162         }
 163     }
 164 
 165     /**
 166      * Extra interfaces to be added.
 167      */
 168     enum MultiInterfacesKind implements ComboParameter {
 169         NONE(new Class<?>[0]),
 170         INTERFACE_WITH_EXCEPTION(new Class<?>[] {InterfaceWithException.class}),
 171         ;
 172 
 173         Class<?>[] multiInterfaceClasses;
 174 
 175         MultiInterfacesKind(Class<?>[] multiInterfaceClasses) {
 176             this.multiInterfaceClasses = multiInterfaceClasses;
 177         }
 178 
 179         @Override
 180         // Not used for expansion only execution
 181         public String expand(String optParameter) {
 182             throw new RuntimeException("NYI");
 183         }
 184 
 185         Class<?>[] classes() {
 186             return multiInterfaceClasses;
 187         }
 188     }
 189 
 190     @Override
 191     public int id() {
 192         return ++uniqueId;
 193     }
 194 
 195     protected void fail(String msg, Throwable thrown) {
 196         super.fail(msg);
 197         thrown.printStackTrace();
 198     }
 199 
 200     /**
 201      * Test interface with a "one(int)" method.
 202      */
 203     interface InterfaceWithException {
 204         // The signature must match the ONE MethodsKind above
 205         void one(int a) throws RuntimeException, IOException;
 206     }
 207 
 208 
 209     /**
 210      * Main to generate combinations and run the tests.
 211      * @param args unused
 212      * @throws Exception In case of failure
 213      */
 214     public static void main(String... args) throws Exception {
 215 
 216         // Test variations of access declarations
 217         new ComboTestHelper<ProxyGeneratorCombo>()
 218                 .withDimension("CLASSACCESS", ClassAccessKind.values())
 219                 .withDimension("METHODACCESS", new ClassAccessKind[]{ClassAccessKind.PUBLIC})
 220                 .withDimension("METHODS", ProxyGeneratorCombo::saveMethod,
 221                         new MethodsKind[] {MethodsKind.NONE, MethodsKind.ZERO, MethodsKind.ONE})
 222                 .withDimension("ARG[0]", new ArgumentKind[] {ArgumentKind.INT})
 223                 .withDimension("EXCEPTION", ProxyGeneratorCombo::saveException,
 224                         new ExceptionKind[]{ExceptionKind.NONE})
 225                 .run(ProxyGeneratorCombo::new);
 226 
 227         // Test variations of argument types
 228         new ComboTestHelper<ProxyGeneratorCombo>()
 229                 .withDimension("CLASSACCESS", new ClassAccessKind[]{ClassAccessKind.PUBLIC})
 230                 .withDimension("METHODACCESS", new ClassAccessKind[]{ClassAccessKind.PUBLIC})
 231                 .withDimension("METHODS", ProxyGeneratorCombo::saveMethod,
 232                         MethodsKind.values())
 233                 .withArrayDimension("ARG", ProxyGeneratorCombo::saveArg, 2,
 234                         ArgumentKind.values())
 235                 .withDimension("EXCEPTION", ProxyGeneratorCombo::saveException,
 236                         new ExceptionKind[]{ExceptionKind.NONE})
 237                 .withFilter(ProxyGeneratorCombo::filter)
 238                 .run(ProxyGeneratorCombo::new);
 239 
 240         // Test for conflicts in Exceptions on methods with the same signatures
 241         new ComboTestHelper<ProxyGeneratorCombo>()
 242                 .withDimension("CLASSACCESS", new ClassAccessKind[]{ClassAccessKind.PUBLIC})
 243                 .withDimension("METHODACCESS", new ClassAccessKind[]{ClassAccessKind.PUBLIC})
 244                 .withDimension("METHODS", ProxyGeneratorCombo::saveMethod, new MethodsKind[] {
 245                         MethodsKind.ZERO})
 246                 .withDimension("EXCEPTION", ProxyGeneratorCombo::saveException,
 247                         ExceptionKind.values())
 248                 .withDimension("MULTI_INTERFACES", ProxyGeneratorCombo::saveInterface,
 249                         new MultiInterfacesKind[] {MultiInterfacesKind.NONE})
 250                 .run(ProxyGeneratorCombo::new);
 251     }
 252 
 253     /**
 254      * Basic template.
 255      */
 256     String template = "#{CLASSACCESS} interface #{TESTNAME} {\n" +
 257             "#{METHODS}" +
 258             "}";
 259 
 260     // Saved values of Combo values
 261     private MultiInterfacesKind currInterface = MultiInterfacesKind.NONE;
 262     private MethodsKind currMethod = MethodsKind.NONE;
 263     private ExceptionKind currException = ExceptionKind.NONE;
 264     private ArgumentKind[] currArgs = new ArgumentKind[0];
 265 
 266     void saveInterface(ComboParameter s) {
 267         currInterface = (MultiInterfacesKind)s;
 268     }
 269 
 270     void saveMethod(ComboParameter s) {
 271         currMethod = (MethodsKind)s;
 272     }
 273 
 274     void saveException(ComboParameter s) {
 275         currException = (ExceptionKind)s;
 276     }
 277 
 278     void saveArg(ComboParameter s, int index) {
 279         if (index >= currArgs.length) {
 280             currArgs = Arrays.copyOf(currArgs, index + 1);
 281         }
 282         currArgs[index] = (ArgumentKind)s;
 283     }
 284 
 285     /**
 286      * Filter out needless tests (mostly with more variations of arguments than needed).
 287      * @return true to run the test, false if not
 288      */
 289     boolean filter() {
 290         if ((currMethod == MethodsKind.NONE || currMethod == MethodsKind.ZERO) &&
 291                 currArgs.length >= 2) {
 292             return currArgs[0] == ArgumentKind.INT &&
 293                 currArgs[1] == ArgumentKind.INT;
 294         }
 295         if (currMethod == MethodsKind.ONE &&
 296                 currArgs.length >= 2 ) {
 297             return currArgs[0] == currArgs[1];
 298         }
 299         return true;
 300     }
 301 
 302     /**
 303      * Generate the source file and compile.
 304      * Generate a proxy for the interface and test the resulting Proxy
 305      * for the methods, exceptions and handling of a thrown exception
 306      * @throws IOException catch all IOException
 307      */
 308     @Override
 309     public void doWork() throws IOException {
 310         String cp = System.getProperty("test.classes");
 311         String ifaceName = "Interface_" + this.id();
 312         newCompilationTask()
 313                 .withSourceFromTemplate(ifaceName, template.replace("#{TESTNAME}", ifaceName))
 314                 .withOption("-d")
 315                 .withOption(cp)
 316                 .generate(this::checkCompile);
 317         try {
 318             ClassLoader loader = ClassLoader.getSystemClassLoader();
 319             Class<?> tc = Class.forName(ifaceName);
 320             InvocationHandler handler =
 321                     new ProxyHandler(currException.exceptionKindClass);
 322 
 323             // Construct array of interfaces for the proxy
 324             Class<?>[] interfaces = new Class<?>[currInterface.classes().length + 1];
 325             interfaces[0] = tc;
 326             System.arraycopy(currInterface.classes(), 0,
 327                     interfaces, 1,
 328                     currInterface.classes().length);
 329 
 330             Object proxy = Proxy.newProxyInstance(loader, interfaces, handler);
 331             if (!Proxy.isProxyClass(proxy.getClass())) {
 332                 fail("generated proxy is not a proxy class");
 333                 return;
 334             }
 335             for (Class<?> i : interfaces) {
 336                 if (!i.isAssignableFrom(proxy.getClass())) {
 337                     fail("proxy is not assignable to " + i.getName());
 338                 }
 339             }
 340             try {
 341                 String s = proxy.toString();
 342             } catch (Exception ex) {
 343                 ex.printStackTrace();
 344                 fail("proxy.toString() threw an exception");
 345             }
 346 
 347             checkDeclaredProxyExceptions(proxy, interfaces);
 348 
 349             if (currMethod == MethodsKind.ZERO && currException != ExceptionKind.NONE) {
 350                 checkThrowsException(proxy, interfaces);
 351             }
 352 
 353         } catch (Exception ex) {
 354             throw new RuntimeException("doWork unexpected", ex);
 355         }
 356     }
 357 
 358     /**
 359      * Check that the exceptions declared on the proxy match the declarations for
 360      * exceptions from the interfaces.
 361      *
 362      * @param proxy a proxy object
 363      * @param interfaces the interfaces that defined it
 364      */
 365     void checkDeclaredProxyExceptions(Object proxy, Class<?>[] interfaces) {
 366         var allMethods = allMethods(Arrays.asList(interfaces));
 367         Method[] methods = proxy.getClass().getDeclaredMethods();
 368         for (Method m : methods) {
 369             String sig = toShortSignature(m);
 370             var imethods = allMethods.get(sig);
 371             if (imethods != null) {
 372                 var expectedEx = Set.copyOf(Arrays.asList(m.getExceptionTypes()));
 373                 var exs = Set.copyOf(extractExceptions(imethods));
 374                 if (!expectedEx.equals(exs)) {
 375                     System.out.printf("mismatch on exceptions for method %s:%nExpected: " +
 376                                     "%s%nActual:  %s%n",
 377                             sig, expectedEx, exs);
 378                     fail("Exceptions declared on proxy don't match interface methods");
 379                 }
 380             }
 381         }
 382     }
 383 
 384     void checkThrowsException(Object proxy, Class<?>[] interfaces) {
 385         ProxyHandler ph = (ProxyHandler)(Proxy.getInvocationHandler(proxy));
 386         try {
 387             Method m = proxy.getClass().getDeclaredMethod("zero");
 388             m.invoke(proxy);
 389             fail("Missing exception: " + ph.exceptionClass);
 390         } catch (NoSuchMethodException nsme) {
 391             System.out.printf("No method 'zero()' to test exceptions with%n");
 392             for (var cl : interfaces) {
 393                 System.out.printf("     i/f %s: %s%n", cl, Arrays.toString(cl.getMethods()));
 394             }
 395             Method[] methods = proxy.getClass().getMethods();
 396             System.out.printf("    Proxy methods: %s%n", Arrays.toString(methods));
 397             fail("No such method test bug", nsme);
 398         } catch (InvocationTargetException actual) {
 399             ph.checkThrownException(actual.getTargetException());
 400         } catch (IllegalAccessException iae) {
 401             fail("IllegalAccessException", iae);
 402         }
 403     }
 404 
 405     /**
 406      * Exceptions known to be supported by all methods with the same signature.
 407      * @return a list of universal exception types
 408      */
 409     private static List<Class<?>> extractExceptions(List<Method> methods) {
 410         // for all methods with the same signature
 411         // start with the exceptions from the first method
 412         // while there are any exceptions remaining
 413         // look at the next method
 414         List<Class<?>> exceptions = null;
 415         for (Method m : methods) {
 416             var e = m.getExceptionTypes();
 417             if (e.length == 0)
 418                 return emptyClassList();
 419             List<Class<?>> elist = Arrays.asList(e);
 420             if (exceptions == null) {
 421                 exceptions = elist;    // initialize to first method exceptions
 422             } else {
 423                 // for each exception
 424                 // if it is compatible (both ways) with any of the existing exceptions continue
 425                 //    else remove the current exception
 426                 var okExceptions = new HashSet<Class<?>>();
 427                 for (int j = 0; j < exceptions.size(); j++) {
 428                     var ex = exceptions.get(j);
 429                     for (int i = 0; i < elist.size();i++) {
 430                         var ci = elist.get(i);
 431 
 432                         if (ci.isAssignableFrom(ex)) {
 433                             okExceptions.add(ex);
 434                         }
 435                         if (ex.isAssignableFrom(ci)) {
 436                             okExceptions.add(ci);
 437                         }
 438                     }
 439                 }
 440                 if (exceptions.isEmpty()) {
 441                     // The empty set terminates the search for a common set of exceptions
 442                     return emptyClassList();
 443                 }
 444                 // Use the new set for the next iteration
 445                 exceptions = List.copyOf(okExceptions);
 446             }
 447         }
 448         return (exceptions == null) ? emptyClassList() : exceptions;
 449     }
 450 
 451     /**
 452      * An empty correctly typed list of classes.
 453      * @return An empty typed list of classes
 454      */
 455     @SuppressWarnings("unchecked")
 456     static List<Class<?>> emptyClassList() {
 457         return Collections.EMPTY_LIST;
 458     }
 459 
 460     /**
 461      * Accumulate all of the unique methods.
 462      *
 463      * @param interfaces a list of interfaces
 464      * @return a map from signature to List of methods, unique by signature
 465      */
 466     private static Map<String, List<Method>> allMethods(List<Class<?>> interfaces) {
 467         Map<String, List<Method>> methods = new HashMap<>();
 468         for (Class<?> c : interfaces) {
 469             for (Method m : c.getMethods()) {
 470                 if (!Modifier.isStatic(m.getModifiers())) {
 471                     String sig = toShortSignature(m);
 472                     methods.computeIfAbsent(sig, s -> new ArrayList<Method>())
 473                             .add(m);
 474                 }
 475             }
 476         }
 477         return methods;
 478     }
 479 
 480     /**
 481      * The signature of a method without the return type.
 482      * @param m a Method
 483      * @return the signature with method name and parameters
 484      */
 485     static String toShortSignature(Method m) {
 486         StringJoiner sj = new StringJoiner(",", m.getName() + "(", ")");
 487         for (Class<?> parameterType : m.getParameterTypes()) {
 488             sj.add(parameterType.getTypeName());
 489         }
 490         return sj.toString();
 491     }
 492 
 493     /**
 494      * Report any compilation errors.
 495      * @param res the result
 496      */
 497     void checkCompile(Result<?> res) {
 498         if (res.hasErrors()) {
 499             fail("invalid diagnostics for source:\n" +
 500                     res.compilationInfo() +
 501                     "\nFound error: " + res.hasErrors());
 502         }
 503     }
 504 
 505     /**
 506      * The Handler for the proxy includes the method to invoke the proxy
 507      * and the expected exception, if any.
 508      */
 509     class ProxyHandler implements InvocationHandler {
 510 
 511         private final Class<? extends Throwable> exceptionClass;
 512 
 513         ProxyHandler(Class<? extends Throwable> throwable) {
 514             this.exceptionClass = throwable;
 515         }
 516 
 517         /**
 518          * Invoke a method on the proxy or return a value.
 519          * @param   proxy the proxy instance that the method was invoked on
 520          * @param   method a method
 521          * @param   args some args
 522          * @return
 523          * @throws Throwable a throwable
 524          */
 525         @Override
 526         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
 527             if (method.getName().equals("toString")) {
 528                 return "Proxy" + System.identityHashCode(proxy);
 529             }
 530             if (method.getName().equals("zero")) {
 531                 if (exceptionClass != null) {
 532                     throw exceptionClass.getDeclaredConstructor().newInstance();
 533                 }
 534             }
 535             return "meth: " + method.toString();
 536         }
 537 
 538         /**
 539          * Check that the expected exception was thrown.
 540          * Special case is handled for Exception1 which does not appear in the
 541          * throws clause of the method so UndeclaredThrowableException is expected.
 542          */
 543         void checkThrownException(Throwable thrown) {
 544             if (exceptionClass == Exception1.class &&
 545                     thrown instanceof UndeclaredThrowableException &&
 546                     ((UndeclaredThrowableException)thrown).getUndeclaredThrowable() instanceof Exception1) {
 547                 // Exception1 caused UndeclaredThrowableException
 548                 return;
 549             } else if (exceptionClass == Exception1.class) {
 550                 fail("UndeclaredThrowableException", thrown);
 551             }
 552 
 553             if (exceptionClass != null &&
 554                     !exceptionClass.equals(thrown.getClass())) {
 555                 throw new RuntimeException("Wrong exception thrown: expected: " + exceptionClass +
 556                         ", actual: " + thrown.getClass());
 557             }
 558         }
 559     }
 560 
 561     /**
 562      * Exception to be thrown as a test of InvocationTarget.
 563      */
 564     static class Exception1 extends Exception {
 565         private static final long serialVersionUID = 1L;
 566         Exception1() {}
 567     }
 568 }