1 /*
   2  * Copyright (c) 2016, 2017, 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 import javax.tools.Diagnostic;
  25 import javax.tools.DiagnosticListener;
  26 import javax.tools.FileObject;
  27 import javax.tools.ForwardingJavaFileManager;
  28 import javax.tools.JavaCompiler;
  29 import javax.tools.JavaFileObject;
  30 import javax.tools.SimpleJavaFileObject;
  31 import javax.tools.StandardJavaFileManager;
  32 import javax.tools.StandardLocation;
  33 import javax.tools.ToolProvider;
  34 import java.io.BufferedReader;
  35 import java.io.ByteArrayOutputStream;
  36 import java.io.Closeable;
  37 import java.io.IOException;
  38 import java.io.InputStreamReader;
  39 import java.io.OutputStream;
  40 import java.io.UncheckedIOException;
  41 import java.lang.reflect.Method;
  42 import java.net.URI;
  43 import java.nio.charset.Charset;
  44 import java.util.ArrayList;
  45 import java.util.HashMap;
  46 import java.util.List;
  47 import java.util.Locale;
  48 import java.util.Map;
  49 import java.util.regex.Pattern;
  50 import java.util.stream.Collectors;
  51 import java.util.stream.IntStream;
  52 import java.util.stream.Stream;
  53 
  54 import static java.util.stream.Collectors.joining;
  55 import static java.util.stream.Collectors.toMap;
  56 
  57 /*
  58  * @test
  59  * @bug 8062389
  60  * @modules java.compiler
  61  *          jdk.compiler
  62  *          jdk.zipfs
  63  * @summary Nearly exhaustive test of Class.getMethod() and Class.getMethods()
  64  * @run main PublicMethodsTest
  65  */
  66 public class PublicMethodsTest {
  67 
  68     public static void main(String[] args) {
  69         Case c = new Case1();
  70 
  71         int[] diffs = new int[1];
  72         try (Stream<Map.Entry<int[], Map<String, String>>>
  73                  expected = expectedResults(c)) {
  74             diffResults(c, expected)
  75                 .forEach(diff -> {
  76                     System.out.println(diff);
  77                     diffs[0]++;
  78                 });
  79         }
  80 
  81         if (diffs[0] > 0) {
  82             throw new RuntimeException(
  83                 "There were " + diffs[0] + " differences.");
  84         }
  85     }
  86 
  87     // use this to generate .results file for particular case
  88     public static class Generate {
  89         public static void main(String[] args) {
  90             Case c = new Case1();
  91             dumpResults(generateResults(c))
  92                 .forEach(System.out::println);
  93         }
  94     }
  95 
  96     interface Case {
  97         Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\$\\{(.+?)}");
  98 
  99         // possible variants of interface method
 100         List<String> INTERFACE_METHODS = List.of(
 101             "", "void m();", "default void m() {}", "static void m() {}"
 102         );
 103 
 104         // possible variants of class method
 105         List<String> CLASS_METHODS = List.of(
 106             "", "public abstract void m();",
 107             "public void m() {}", "public static void m() {}"
 108         );
 109 
 110         // template with placeholders parsed with PLACEHOLDER_PATTERN
 111         String template();
 112 
 113         // map of replacementKey (== PLACEHOLDER_PATTERN captured group #1) ->
 114         // list of possible replacements
 115         Map<String, List<String>> replacements();
 116 
 117         // ordered list of replacement keys
 118         List<String> replacementKeys();
 119 
 120         // names of types occurring in the template
 121         List<String> classNames();
 122     }
 123 
 124     static class Case1 implements Case {
 125 
 126         private static final String TEMPLATE = Stream.of(
 127             "interface I { ${I} }",
 128             "interface J { ${J} }",
 129             "interface K extends I, J { ${K} }",
 130             "abstract class C { ${C} }",
 131             "abstract class D extends C implements I { ${D} }",
 132             "abstract class E extends D implements J, K { ${E} }"
 133         ).collect(joining("\n"));
 134 
 135         private static final Map<String, List<String>> REPLACEMENTS = Map.of(
 136             "I", INTERFACE_METHODS,
 137             "J", INTERFACE_METHODS,
 138             "K", INTERFACE_METHODS,
 139             "C", CLASS_METHODS,
 140             "D", CLASS_METHODS,
 141             "E", CLASS_METHODS
 142         );
 143 
 144         private static final List<String> REPLACEMENT_KEYS = REPLACEMENTS
 145             .keySet().stream().sorted().collect(Collectors.toList());
 146 
 147         @Override
 148         public String template() {
 149             return TEMPLATE;
 150         }
 151 
 152         @Override
 153         public Map<String, List<String>> replacements() {
 154             return REPLACEMENTS;
 155         }
 156 
 157         @Override
 158         public List<String> replacementKeys() {
 159             return REPLACEMENT_KEYS;
 160         }
 161 
 162         @Override
 163         public List<String> classNames() {
 164             // just by accident, names of classes are equal to replacement keys
 165             // (this need not be the case in general)
 166             return REPLACEMENT_KEYS;
 167         }
 168     }
 169 
 170     // generate all combinations as a tuple of indexes into lists of
 171     // replacements. The index of the element in int[] tuple represents the index
 172     // of the key in replacementKeys() list. The value of the element in int[] tuple
 173     // represents the index of the replacement string in list of strings in the
 174     // value of the entry of replacements() map with the corresponding key.
 175     static Stream<int[]> combinations(Case c) {
 176         int[] sizes = c.replacementKeys().stream()
 177                        .mapToInt(key -> c.replacements().get(key).size())
 178                        .toArray();
 179 
 180         return Stream.iterate(
 181             new int[sizes.length],
 182             state -> state != null,
 183             state -> {
 184                 int[] newState = state.clone();
 185                 for (int i = 0; i < state.length; i++) {
 186                     if (++newState[i] < sizes[i]) {
 187                         return newState;
 188                     }
 189                     newState[i] = 0;
 190                 }
 191                 // wrapped-around
 192                 return null;
 193             }
 194         );
 195     }
 196 
 197     // given the combination of indexes, return the expanded template
 198     static String expandTemplate(Case c, int[] combination) {
 199 
 200         // 1st create a map: key -> replacement string
 201         Map<String, String> map = new HashMap<>(combination.length * 4 / 3 + 1);
 202         for (int i = 0; i < combination.length; i++) {
 203             String key = c.replacementKeys().get(i);
 204             String repl = c.replacements().get(key).get(combination[i]);
 205             map.put(key, repl);
 206         }
 207 
 208         return Case.PLACEHOLDER_PATTERN
 209             .matcher(c.template())
 210             .replaceAll(match -> map.get(match.group(1)));
 211     }
 212 
 213     /**
 214      * compile expanded template into a ClassLoader that sees compiled classes
 215      */
 216     static TestClassLoader compile(String source) throws CompileException {
 217         JavaCompiler javac = ToolProvider.getSystemJavaCompiler();
 218         if (javac == null) {
 219             throw new AssertionError("No Java compiler tool found.");
 220         }
 221 
 222         ErrorsCollector errorsCollector = new ErrorsCollector();
 223         StandardJavaFileManager standardJavaFileManager =
 224             javac.getStandardFileManager(errorsCollector, Locale.ROOT,
 225                                          Charset.forName("UTF-8"));
 226         TestFileManager testFileManager = new TestFileManager(
 227             standardJavaFileManager, source);
 228 
 229         JavaCompiler.CompilationTask javacTask;
 230         try {
 231             javacTask = javac.getTask(
 232                 null, // use System.err
 233                 testFileManager,
 234                 errorsCollector,
 235                 null,
 236                 null,
 237                 List.of(testFileManager.getJavaFileForInput(
 238                     StandardLocation.SOURCE_PATH,
 239                     TestFileManager.TEST_CLASS_NAME,
 240                     JavaFileObject.Kind.SOURCE))
 241             );
 242         } catch (IOException e) {
 243             throw new UncheckedIOException(e);
 244         }
 245 
 246         javacTask.call();
 247 
 248         if (errorsCollector.hasError()) {
 249             throw new CompileException(errorsCollector.getErrors());
 250         }
 251 
 252         return new TestClassLoader(ClassLoader.getSystemClassLoader(),
 253                                    testFileManager);
 254     }
 255 
 256     static class CompileException extends Exception {
 257         CompileException(List<Diagnostic<?>> diagnostics) {
 258             super(diagnostics.stream()
 259                              .map(diag -> diag.toString())
 260                              .collect(Collectors.joining("\n")));
 261         }
 262     }
 263 
 264     static class TestFileManager
 265         extends ForwardingJavaFileManager<StandardJavaFileManager> {
 266         static final String TEST_CLASS_NAME = "Test";
 267 
 268         private final String testSource;
 269         private final Map<String, ClassFileObject> classes = new HashMap<>();
 270 
 271         TestFileManager(StandardJavaFileManager fileManager, String source) {
 272             super(fileManager);
 273             testSource = "public class " + TEST_CLASS_NAME + " {}\n" +
 274                          source; // the rest of classes are package-private
 275         }
 276 
 277         @Override
 278         public JavaFileObject getJavaFileForInput(Location location,
 279                                                   String className,
 280                                                   JavaFileObject.Kind kind)
 281         throws IOException {
 282             if (location == StandardLocation.SOURCE_PATH &&
 283                 kind == JavaFileObject.Kind.SOURCE &&
 284                 TEST_CLASS_NAME.equals(className)) {
 285                 return new SourceFileObject(className, testSource);
 286             }
 287             return super.getJavaFileForInput(location, className, kind);
 288         }
 289 
 290         private static class SourceFileObject extends SimpleJavaFileObject {
 291             private final String source;
 292 
 293             SourceFileObject(String className, String source) {
 294                 super(
 295                     URI.create("memory:/src/" +
 296                                className.replace('.', '/') + ".java"),
 297                     Kind.SOURCE
 298                 );
 299                 this.source = source;
 300             }
 301 
 302             @Override
 303             public CharSequence getCharContent(boolean ignoreEncodingErrors) {
 304                 return source;
 305             }
 306         }
 307 
 308         @Override
 309         public JavaFileObject getJavaFileForOutput(Location location,
 310                                                    String className,
 311                                                    JavaFileObject.Kind kind,
 312                                                    FileObject sibling)
 313         throws IOException {
 314             if (kind == JavaFileObject.Kind.CLASS) {
 315                 ClassFileObject cfo = new ClassFileObject(className);
 316                 classes.put(className, cfo);
 317                 return cfo;
 318             }
 319             return super.getJavaFileForOutput(location, className, kind, sibling);
 320         }
 321 
 322         private static class ClassFileObject extends SimpleJavaFileObject {
 323             final String className;
 324             ByteArrayOutputStream byteArrayOutputStream;
 325 
 326             ClassFileObject(String className) {
 327                 super(
 328                     URI.create("memory:/out/" +
 329                                className.replace('.', '/') + ".class"),
 330                     Kind.CLASS
 331                 );
 332                 this.className = className;
 333             }
 334 
 335             @Override
 336             public OutputStream openOutputStream() throws IOException {
 337                 return byteArrayOutputStream = new ByteArrayOutputStream();
 338             }
 339 
 340             byte[] getBytes() {
 341                 if (byteArrayOutputStream == null) {
 342                     throw new IllegalStateException(
 343                         "No class file written for class: " + className);
 344                 }
 345                 return byteArrayOutputStream.toByteArray();
 346             }
 347         }
 348 
 349         byte[] getClassBytes(String className) {
 350             ClassFileObject cfo = classes.get(className);
 351             return (cfo == null) ? null : cfo.getBytes();
 352         }
 353     }
 354 
 355     static class ErrorsCollector implements DiagnosticListener<JavaFileObject> {
 356         private final List<Diagnostic<?>> errors = new ArrayList<>();
 357 
 358         @Override
 359         public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
 360             if (diagnostic.getKind() == Diagnostic.Kind.ERROR) {
 361                 errors.add(diagnostic);
 362             }
 363         }
 364 
 365         boolean hasError() {
 366             return !errors.isEmpty();
 367         }
 368 
 369         List<Diagnostic<?>> getErrors() {
 370             return errors;
 371         }
 372     }
 373 
 374     static class TestClassLoader extends ClassLoader implements Closeable {
 375         private final TestFileManager fileManager;
 376 
 377         public TestClassLoader(ClassLoader parent, TestFileManager fileManager) {
 378             super(parent);
 379             this.fileManager = fileManager;
 380         }
 381 
 382         @Override
 383         protected Class<?> findClass(String name) throws ClassNotFoundException {
 384             byte[] classBytes = fileManager.getClassBytes(name);
 385             if (classBytes == null) {
 386                 throw new ClassNotFoundException(name);
 387             }
 388             return defineClass(name, classBytes, 0, classBytes.length);
 389         }
 390 
 391         @Override
 392         public void close() throws IOException {
 393             fileManager.close();
 394         }
 395     }
 396 
 397     static Map<String, String> generateResult(Case c, ClassLoader cl) {
 398         return
 399             c.classNames()
 400              .stream()
 401              .map(cn -> {
 402                  try {
 403                      return Class.forName(cn, false, cl);
 404                  } catch (ClassNotFoundException e) {
 405                      throw new RuntimeException("Class not found: " + cn, e);
 406                  }
 407              })
 408              .flatMap(clazz -> Stream.of(
 409                  Map.entry(clazz.getName() + ".gM", generateGetMethodResult(clazz)),
 410                  Map.entry(clazz.getName() + ".gMs", generateGetMethodsResult(clazz))
 411              ))
 412              .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
 413     }
 414 
 415     static String generateGetMethodResult(Class<?> clazz) {
 416         try {
 417             Method m = clazz.getMethod("m");
 418             return m.getDeclaringClass().getName() + "." + m.getName();
 419         } catch (NoSuchMethodException e) {
 420             return "-";
 421         }
 422     }
 423 
 424     static String generateGetMethodsResult(Class<?> clazz) {
 425         return Stream.of(clazz.getMethods())
 426                      .filter(m -> m.getDeclaringClass() != Object.class)
 427                      .map(m -> m.getDeclaringClass().getName()
 428                                + "." + m.getName())
 429                      .collect(Collectors.joining(", ", "[", "]"));
 430     }
 431 
 432     static Stream<Map.Entry<int[], Map<String, String>>> generateResults(Case c) {
 433         return combinations(c)
 434             .flatMap(comb -> {
 435                 String src = expandTemplate(c, comb);
 436                 try {
 437                     try (TestClassLoader cl = compile(src)) {
 438                         // compilation was successful -> generate result
 439                         return Stream.of(Map.entry(
 440                             comb,
 441                             generateResult(c, cl)
 442                         ));
 443                     } catch (CompileException e) {
 444                         // ignore uncompilable combinations
 445                         return Stream.empty();
 446                     }
 447                 } catch (IOException ioe) {
 448                     // from TestClassLoader.close()
 449                     throw new UncheckedIOException(ioe);
 450                 }
 451             });
 452     }
 453 
 454     static Stream<Map.Entry<int[], Map<String, String>>> expectedResults(Case c) {
 455         try {
 456             BufferedReader r = new BufferedReader(new InputStreamReader(
 457                 c.getClass().getResourceAsStream(
 458                     c.getClass().getSimpleName() + ".results"),
 459                 "UTF-8"
 460             ));
 461 
 462             return parseResults(r.lines())
 463                 .onClose(() -> {
 464                     try {
 465                         r.close();
 466                     } catch (IOException ioe) {
 467                         throw new UncheckedIOException(ioe);
 468                     }
 469                 });
 470         } catch (IOException e) {
 471             throw new UncheckedIOException(e);
 472         }
 473     }
 474 
 475     static Stream<Map.Entry<int[], Map<String, String>>> parseResults(
 476         Stream<String> lines
 477     ) {
 478         return lines
 479             .map(l -> l.split(Pattern.quote("#")))
 480             .map(lkv -> Map.entry(
 481                 Stream.of(lkv[0].split(Pattern.quote(",")))
 482                       .mapToInt(Integer::parseInt)
 483                       .toArray(),
 484                 Stream.of(lkv[1].split(Pattern.quote("|")))
 485                       .map(e -> e.split(Pattern.quote("=")))
 486                       .collect(toMap(ekv -> ekv[0], ekv -> ekv[1]))
 487             ));
 488     }
 489 
 490     static Stream<String> dumpResults(
 491         Stream<Map.Entry<int[], Map<String, String>>> results
 492     ) {
 493         return results
 494             .map(le ->
 495                      IntStream.of(le.getKey())
 496                               .mapToObj(String::valueOf)
 497                               .collect(joining(","))
 498                      + "#" +
 499                      le.getValue().entrySet().stream()
 500                        .map(e -> e.getKey() + "=" + e.getValue())
 501                        .collect(joining("|"))
 502             );
 503     }
 504 
 505     static Stream<String> diffResults(
 506         Case c,
 507         Stream<Map.Entry<int[], Map<String, String>>> expectedResults
 508     ) {
 509         return expectedResults
 510             .flatMap(exp -> {
 511                 int[] comb = exp.getKey();
 512                 Map<String, String> expected = exp.getValue();
 513 
 514                 String src = expandTemplate(c, comb);
 515                 Map<String, String> actual;
 516                 try {
 517                     try (TestClassLoader cl = compile(src)) {
 518                         actual = generateResult(c, cl);
 519                     } catch (CompileException ce) {
 520                         return Stream.of(src + "\n" +
 521                                          "got compilation error: " + ce);
 522                     }
 523                 } catch (IOException ioe) {
 524                     // from TestClassLoader.close()
 525                     return Stream.of(src + "\n" +
 526                                      "got IOException: " + ioe);
 527                 }
 528 
 529                 if (actual.equals(expected)) {
 530                     return Stream.empty();
 531                 } else {
 532                     Map<String, String> diff = new HashMap<>(expected);
 533                     diff.entrySet().removeAll(actual.entrySet());
 534                     return Stream.of(
 535                         diff.entrySet()
 536                             .stream()
 537                             .map(e -> "expected: " + e.getKey() + ": " +
 538                                       e.getValue() + "\n" +
 539                                       "  actual: " + e.getKey() + ": " +
 540                                       actual.get(e.getKey()) + "\n")
 541                             .collect(joining("\n", src + "\n\n", "\n"))
 542                     );
 543                 }
 544             });
 545     }
 546 }