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