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 
main(String[] args)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 {
main(String[] args)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
template()110         String template();
111 
112         // map of replacementKey (== PLACEHOLDER_PATTERN captured group #1) ->
113         // list of possible replacements
replacements()114         Map<String, List<String>> replacements();
115 
116         // ordered list of replacement keys
replacementKeys()117         List<String> replacementKeys();
118 
119         // names of types occurring in the template
classNames()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
template()147         public String template() {
148             return TEMPLATE;
149         }
150 
151         @Override
replacements()152         public Map<String, List<String>> replacements() {
153             return REPLACEMENTS;
154         }
155 
156         @Override
replacementKeys()157         public List<String> replacementKeys() {
158             return REPLACEMENT_KEYS;
159         }
160 
161         @Override
classNames()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.
combinations(Case c)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
expandTemplate(Case c, int[] combination)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      */
compile(String source)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         try {
226             standardJavaFileManager.setLocation(StandardLocation.CLASS_PATH, List.of());
227         } catch (IOException e) {
228             throw new UncheckedIOException(e);
229         }
230         TestFileManager testFileManager = new TestFileManager(
231             standardJavaFileManager, source);
232 
233         JavaCompiler.CompilationTask javacTask;
234         try {
235             javacTask = javac.getTask(
236                 null, // use System.err
237                 testFileManager,
238                 errorsCollector,
239                 null,
240                 null,
241                 List.of(testFileManager.getJavaFileForInput(
242                     StandardLocation.SOURCE_PATH,
243                     TestFileManager.TEST_CLASS_NAME,
244                     JavaFileObject.Kind.SOURCE))
245             );
246         } catch (IOException e) {
247             throw new UncheckedIOException(e);
248         }
249 
250         javacTask.call();
251 
252         if (errorsCollector.hasError()) {
253             throw new CompileException(errorsCollector.getErrors());
254         }
255 
256         return new TestClassLoader(ClassLoader.getSystemClassLoader(),
257                                    testFileManager);
258     }
259 
260     static class CompileException extends Exception {
CompileException(List<Diagnostic<?>> diagnostics)261         CompileException(List<Diagnostic<?>> diagnostics) {
262             super(diagnostics.stream()
263                              .map(diag -> diag.toString())
264                              .collect(Collectors.joining("\n")));
265         }
266     }
267 
268     static class TestFileManager
269         extends ForwardingJavaFileManager<StandardJavaFileManager> {
270         static final String TEST_CLASS_NAME = "Test";
271 
272         private final String testSource;
273         private final Map<String, ClassFileObject> classes = new HashMap<>();
274 
TestFileManager(StandardJavaFileManager fileManager, String source)275         TestFileManager(StandardJavaFileManager fileManager, String source) {
276             super(fileManager);
277             testSource = "public class " + TEST_CLASS_NAME + " {}\n" +
278                          source; // the rest of classes are package-private
279         }
280 
281         @Override
getJavaFileForInput(Location location, String className, JavaFileObject.Kind kind)282         public JavaFileObject getJavaFileForInput(Location location,
283                                                   String className,
284                                                   JavaFileObject.Kind kind)
285         throws IOException {
286             if (location == StandardLocation.SOURCE_PATH &&
287                 kind == JavaFileObject.Kind.SOURCE &&
288                 TEST_CLASS_NAME.equals(className)) {
289                 return new SourceFileObject(className, testSource);
290             }
291             return super.getJavaFileForInput(location, className, kind);
292         }
293 
294         private static class SourceFileObject extends SimpleJavaFileObject {
295             private final String source;
296 
SourceFileObject(String className, String source)297             SourceFileObject(String className, String source) {
298                 super(
299                     URI.create("memory:/src/" +
300                                className.replace('.', '/') + ".java"),
301                     Kind.SOURCE
302                 );
303                 this.source = source;
304             }
305 
306             @Override
getCharContent(boolean ignoreEncodingErrors)307             public CharSequence getCharContent(boolean ignoreEncodingErrors) {
308                 return source;
309             }
310         }
311 
312         @Override
getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling)313         public JavaFileObject getJavaFileForOutput(Location location,
314                                                    String className,
315                                                    JavaFileObject.Kind kind,
316                                                    FileObject sibling)
317         throws IOException {
318             if (kind == JavaFileObject.Kind.CLASS) {
319                 ClassFileObject cfo = new ClassFileObject(className);
320                 classes.put(className, cfo);
321                 return cfo;
322             }
323             return super.getJavaFileForOutput(location, className, kind, sibling);
324         }
325 
326         private static class ClassFileObject extends SimpleJavaFileObject {
327             final String className;
328             ByteArrayOutputStream byteArrayOutputStream;
329 
ClassFileObject(String className)330             ClassFileObject(String className) {
331                 super(
332                     URI.create("memory:/out/" +
333                                className.replace('.', '/') + ".class"),
334                     Kind.CLASS
335                 );
336                 this.className = className;
337             }
338 
339             @Override
openOutputStream()340             public OutputStream openOutputStream() throws IOException {
341                 return byteArrayOutputStream = new ByteArrayOutputStream();
342             }
343 
getBytes()344             byte[] getBytes() {
345                 if (byteArrayOutputStream == null) {
346                     throw new IllegalStateException(
347                         "No class file written for class: " + className);
348                 }
349                 return byteArrayOutputStream.toByteArray();
350             }
351         }
352 
getClassBytes(String className)353         byte[] getClassBytes(String className) {
354             ClassFileObject cfo = classes.get(className);
355             return (cfo == null) ? null : cfo.getBytes();
356         }
357     }
358 
359     static class ErrorsCollector implements DiagnosticListener<JavaFileObject> {
360         private final List<Diagnostic<?>> errors = new ArrayList<>();
361 
362         @Override
report(Diagnostic<? extends JavaFileObject> diagnostic)363         public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
364             if (diagnostic.getKind() == Diagnostic.Kind.ERROR) {
365                 errors.add(diagnostic);
366             }
367         }
368 
hasError()369         boolean hasError() {
370             return !errors.isEmpty();
371         }
372 
getErrors()373         List<Diagnostic<?>> getErrors() {
374             return errors;
375         }
376     }
377 
378     static class TestClassLoader extends ClassLoader implements Closeable {
379         private final TestFileManager fileManager;
380 
TestClassLoader(ClassLoader parent, TestFileManager fileManager)381         public TestClassLoader(ClassLoader parent, TestFileManager fileManager) {
382             super(parent);
383             this.fileManager = fileManager;
384         }
385 
386         @Override
findClass(String name)387         protected Class<?> findClass(String name) throws ClassNotFoundException {
388             byte[] classBytes = fileManager.getClassBytes(name);
389             if (classBytes == null) {
390                 throw new ClassNotFoundException(name);
391             }
392             return defineClass(name, classBytes, 0, classBytes.length);
393         }
394 
395         @Override
close()396         public void close() throws IOException {
397             fileManager.close();
398         }
399     }
400 
generateResult(Case c, ClassLoader cl)401     static Map<String, String> generateResult(Case c, ClassLoader cl) {
402         return
403             c.classNames()
404              .stream()
405              .map(cn -> {
406                  try {
407                      return Class.forName(cn, false, cl);
408                  } catch (ClassNotFoundException e) {
409                      throw new RuntimeException("Class not found: " + cn, e);
410                  }
411              })
412              .flatMap(clazz -> Stream.of(
413                  Map.entry(clazz.getName() + ".gM", generateGetMethodResult(clazz)),
414                  Map.entry(clazz.getName() + ".gMs", generateGetMethodsResult(clazz))
415              ))
416              .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
417     }
418 
419     static String generateGetMethodResult(Class<?> clazz) {
420         try {
421             Method m = clazz.getMethod("m");
422             return m.getDeclaringClass().getName() + "." + m.getName();
423         } catch (NoSuchMethodException e) {
424             return "-";
425         }
426     }
427 
428     static String generateGetMethodsResult(Class<?> clazz) {
429         return Stream.of(clazz.getMethods())
430                      .filter(m -> m.getDeclaringClass() != Object.class)
431                      .map(m -> m.getDeclaringClass().getName()
432                                + "." + m.getName())
433                      .collect(Collectors.joining(", ", "[", "]"));
434     }
435 
436     static Stream<Map.Entry<int[], Map<String, String>>> generateResults(Case c) {
437         return combinations(c)
438             .flatMap(comb -> {
439                 String src = expandTemplate(c, comb);
440                 try {
441                     try (TestClassLoader cl = compile(src)) {
442                         // compilation was successful -> generate result
443                         return Stream.of(Map.entry(
444                             comb,
445                             generateResult(c, cl)
446                         ));
447                     } catch (CompileException e) {
448                         // ignore uncompilable combinations
449                         return Stream.empty();
450                     }
451                 } catch (IOException ioe) {
452                     // from TestClassLoader.close()
453                     throw new UncheckedIOException(ioe);
454                 }
455             });
456     }
457 
458     static Stream<Map.Entry<int[], Map<String, String>>> expectedResults(Case c) {
459         try {
460             BufferedReader r = new BufferedReader(new InputStreamReader(
461                 c.getClass().getResourceAsStream(
462                     c.getClass().getSimpleName() + ".results"),
463                 "UTF-8"
464             ));
465 
466             return parseResults(r.lines())
467                 .onClose(() -> {
468                     try {
469                         r.close();
470                     } catch (IOException ioe) {
471                         throw new UncheckedIOException(ioe);
472                     }
473                 });
474         } catch (IOException e) {
475             throw new UncheckedIOException(e);
476         }
477     }
478 
479     static Stream<Map.Entry<int[], Map<String, String>>> parseResults(
480         Stream<String> lines
481     ) {
482         return lines
483             .map(l -> l.split(Pattern.quote("#")))
484             .map(lkv -> Map.entry(
485                 Stream.of(lkv[0].split(Pattern.quote(",")))
486                       .mapToInt(Integer::parseInt)
487                       .toArray(),
488                 Stream.of(lkv[1].split(Pattern.quote("|")))
489                       .map(e -> e.split(Pattern.quote("=")))
490                       .collect(toMap(ekv -> ekv[0], ekv -> ekv[1]))
491             ));
492     }
493 
494     static Stream<String> dumpResults(
495         Stream<Map.Entry<int[], Map<String, String>>> results
496     ) {
497         return results
498             .map(le ->
499                      IntStream.of(le.getKey())
500                               .mapToObj(String::valueOf)
501                               .collect(joining(","))
502                      + "#" +
503                      le.getValue().entrySet().stream()
504                        .map(e -> e.getKey() + "=" + e.getValue())
505                        .collect(joining("|"))
506             );
507     }
508 
509     static Stream<String> diffResults(
510         Case c,
511         Stream<Map.Entry<int[], Map<String, String>>> expectedResults
512     ) {
513         return expectedResults
514             .flatMap(exp -> {
515                 int[] comb = exp.getKey();
516                 Map<String, String> expected = exp.getValue();
517 
518                 String src = expandTemplate(c, comb);
519                 Map<String, String> actual;
520                 try {
521                     try (TestClassLoader cl = compile(src)) {
522                         actual = generateResult(c, cl);
523                     } catch (CompileException ce) {
524                         return Stream.of(src + "\n" +
525                                          "got compilation error: " + ce);
526                     }
527                 } catch (IOException ioe) {
528                     // from TestClassLoader.close()
529                     return Stream.of(src + "\n" +
530                                      "got IOException: " + ioe);
531                 }
532 
533                 if (actual.equals(expected)) {
534                     return Stream.empty();
535                 } else {
536                     Map<String, String> diff = new HashMap<>(expected);
537                     diff.entrySet().removeAll(actual.entrySet());
538                     return Stream.of(
539                         diff.entrySet()
540                             .stream()
541                             .map(e -> "expected: " + e.getKey() + ": " +
542                                       e.getValue() + "\n" +
543                                       "  actual: " + e.getKey() + ": " +
544                                       actual.get(e.getKey()) + "\n")
545                             .collect(joining("\n", src + "\n\n", "\n"))
546                     );
547                 }
548             });
549     }
550 }
551