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
24import javax.tools.Diagnostic;
25import javax.tools.DiagnosticListener;
26import javax.tools.FileObject;
27import javax.tools.ForwardingJavaFileManager;
28import javax.tools.JavaCompiler;
29import javax.tools.JavaFileObject;
30import javax.tools.SimpleJavaFileObject;
31import javax.tools.StandardJavaFileManager;
32import javax.tools.StandardLocation;
33import javax.tools.ToolProvider;
34import java.io.BufferedReader;
35import java.io.ByteArrayOutputStream;
36import java.io.Closeable;
37import java.io.IOException;
38import java.io.InputStreamReader;
39import java.io.OutputStream;
40import java.io.UncheckedIOException;
41import java.lang.reflect.Method;
42import java.net.URI;
43import java.nio.charset.Charset;
44import java.util.ArrayList;
45import java.util.HashMap;
46import java.util.List;
47import java.util.Locale;
48import java.util.Map;
49import java.util.regex.Pattern;
50import java.util.stream.Collectors;
51import java.util.stream.IntStream;
52import java.util.stream.Stream;
53
54import static java.util.stream.Collectors.joining;
55import 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 */
65public 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}
546