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