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 }