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