1 /* 2 * Copyright (c) 2015, 2018, 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 java.io.*; 25 import java.lang.reflect.Method; 26 import java.nio.file.Files; 27 import java.nio.file.Path; 28 import java.nio.file.Paths; 29 import java.util.ArrayList; 30 import java.util.Arrays; 31 import java.util.List; 32 import java.util.function.Consumer; 33 import java.util.jar.JarEntry; 34 import java.util.jar.JarInputStream; 35 import java.util.jar.JarOutputStream; 36 import java.util.stream.Stream; 37 38 import jdk.test.lib.util.FileUtils; 39 import jdk.test.lib.JDKToolFinder; 40 import org.testng.annotations.BeforeTest; 41 import org.testng.annotations.Test; 42 43 import static java.lang.String.format; 44 import static java.lang.System.out; 45 import static java.nio.charset.StandardCharsets.UTF_8; 46 import static org.testng.Assert.assertFalse; 47 import static org.testng.Assert.assertTrue; 48 49 /* 50 * @test 51 * @bug 8170952 52 * @library /lib/testlibrary /test/lib 53 * @build jdk.test.lib.Platform 54 * jdk.test.lib.util.FileUtils 55 * jdk.test.lib.JDKToolFinder 56 * @run testng CLICompatibility 57 * @summary Basic test for compatibility of CLI options 58 */ 59 60 public class CLICompatibility { 61 static final Path TEST_CLASSES = Paths.get(System.getProperty("test.classes", ".")); 62 static final Path USER_DIR = Paths.get(System.getProperty("user.dir")); 63 64 static final String TOOL_VM_OPTIONS = System.getProperty("test.tool.vm.opts", ""); 65 66 final boolean legacyOnly; // for running on older JDK's ( test validation ) 67 68 // Resources we know to exist, that can be used for creating jar files. 69 static final String RES1 = "CLICompatibility.class"; 70 static final String RES2 = "CLICompatibility$Result.class"; 71 72 @BeforeTest setupResourcesForJar()73 public void setupResourcesForJar() throws Exception { 74 // Copy the files that we are going to use for creating/updating test 75 // jar files, so that they can be referred to without '-C dir' 76 Files.copy(TEST_CLASSES.resolve(RES1), USER_DIR.resolve(RES1)); 77 Files.copy(TEST_CLASSES.resolve(RES2), USER_DIR.resolve(RES2)); 78 } 79 80 static final IOConsumer<InputStream> ASSERT_CONTAINS_RES1 = in -> { 81 try (JarInputStream jin = new JarInputStream(in)) { 82 assertTrue(jarContains(jin, RES1), "Failed to find " + RES1); 83 } 84 }; 85 static final IOConsumer<InputStream> ASSERT_CONTAINS_RES2 = in -> { 86 try (JarInputStream jin = new JarInputStream(in)) { 87 assertTrue(jarContains(jin, RES2), "Failed to find " + RES2); 88 } 89 }; 90 static final IOConsumer<InputStream> ASSERT_CONTAINS_MAINFEST = in -> { 91 try (JarInputStream jin = new JarInputStream(in)) { 92 assertTrue(jin.getManifest() != null, "No META-INF/MANIFEST.MF"); 93 } 94 }; 95 static final IOConsumer<InputStream> ASSERT_DOES_NOT_CONTAIN_MAINFEST = in -> { 96 try (JarInputStream jin = new JarInputStream(in)) { 97 assertTrue(jin.getManifest() == null, "Found unexpected META-INF/MANIFEST.MF"); 98 } 99 }; 100 101 static final FailCheckerWithMessage FAIL_TOO_MANY_MAIN_OPS = 102 new FailCheckerWithMessage("You may not specify more than one '-cuxtid' options", 103 /* legacy */ "{ctxui}[vfmn0Me] [jar-file] [manifest-file] [entry-point] [-C dir] files"); 104 105 // Create 106 107 @Test createBadArgs()108 public void createBadArgs() { 109 final FailCheckerWithMessage FAIL_CREATE_NO_ARGS = new FailCheckerWithMessage( 110 "'c' flag requires manifest or input files to be specified!"); 111 112 jar("c") 113 .assertFailure() 114 .resultChecker(FAIL_CREATE_NO_ARGS); 115 116 jar("-c") 117 .assertFailure() 118 .resultChecker(FAIL_CREATE_NO_ARGS); 119 120 if (!legacyOnly) 121 jar("--create") 122 .assertFailure() 123 .resultChecker(FAIL_CREATE_NO_ARGS); 124 125 jar("ct") 126 .assertFailure() 127 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 128 129 jar("-ct") 130 .assertFailure() 131 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 132 133 if (!legacyOnly) 134 jar("--create --list") 135 .assertFailure() 136 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 137 } 138 139 @Test createWriteToFile()140 public void createWriteToFile() throws IOException { 141 Path path = Paths.get("createJarFile.jar"); // for creating 142 String jn = path.toString(); 143 for (String opts : new String[]{"cf " + jn, "-cf " + jn, "--create --file=" + jn}) { 144 if (legacyOnly && opts.startsWith("--")) 145 continue; 146 147 jar(opts, RES1) 148 .assertSuccess() 149 .resultChecker(r -> { 150 ASSERT_CONTAINS_RES1.accept(Files.newInputStream(path)); 151 ASSERT_CONTAINS_MAINFEST.accept(Files.newInputStream(path)); 152 }); 153 } 154 FileUtils.deleteFileIfExistsWithRetry(path); 155 } 156 157 @Test createWriteToStdout()158 public void createWriteToStdout() throws IOException { 159 for (String opts : new String[]{"c", "-c", "--create"}) { 160 if (legacyOnly && opts.startsWith("--")) 161 continue; 162 163 jar(opts, RES1) 164 .assertSuccess() 165 .resultChecker(r -> { 166 ASSERT_CONTAINS_RES1.accept(r.stdoutAsStream()); 167 ASSERT_CONTAINS_MAINFEST.accept(r.stdoutAsStream()); 168 }); 169 } 170 } 171 172 @Test createWriteToStdoutNoManifest()173 public void createWriteToStdoutNoManifest() throws IOException { 174 for (String opts : new String[]{"cM", "-cM", "--create --no-manifest"} ){ 175 if (legacyOnly && opts.startsWith("--")) 176 continue; 177 178 jar(opts, RES1) 179 .assertSuccess() 180 .resultChecker(r -> { 181 ASSERT_CONTAINS_RES1.accept(r.stdoutAsStream()); 182 ASSERT_DOES_NOT_CONTAIN_MAINFEST.accept(r.stdoutAsStream()); 183 }); 184 } 185 } 186 187 // Update 188 189 @Test updateBadArgs()190 public void updateBadArgs() { 191 final FailCheckerWithMessage FAIL_UPDATE_NO_ARGS = new FailCheckerWithMessage( 192 "'u' flag requires manifest, 'e' flag or input files to be specified!"); 193 194 jar("u") 195 .assertFailure() 196 .resultChecker(FAIL_UPDATE_NO_ARGS); 197 198 jar("-u") 199 .assertFailure() 200 .resultChecker(FAIL_UPDATE_NO_ARGS); 201 202 if (!legacyOnly) 203 jar("--update") 204 .assertFailure() 205 .resultChecker(FAIL_UPDATE_NO_ARGS); 206 207 jar("ut") 208 .assertFailure() 209 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 210 211 jar("-ut") 212 .assertFailure() 213 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 214 215 if (!legacyOnly) 216 jar("--update --list") 217 .assertFailure() 218 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 219 } 220 221 @Test updateReadFileWriteFile()222 public void updateReadFileWriteFile() throws IOException { 223 Path path = Paths.get("updateReadWriteStdout.jar"); // for updating 224 String jn = path.toString(); 225 226 for (String opts : new String[]{"uf " + jn, "-uf " + jn, "--update --file=" + jn}) { 227 if (legacyOnly && opts.startsWith("--")) 228 continue; 229 230 createJar(path, RES1); 231 jar(opts, RES2) 232 .assertSuccess() 233 .resultChecker(r -> { 234 ASSERT_CONTAINS_RES1.accept(Files.newInputStream(path)); 235 ASSERT_CONTAINS_RES2.accept(Files.newInputStream(path)); 236 ASSERT_CONTAINS_MAINFEST.accept(Files.newInputStream(path)); 237 }); 238 } 239 FileUtils.deleteFileIfExistsWithRetry(path); 240 } 241 242 @Test updateReadStdinWriteStdout()243 public void updateReadStdinWriteStdout() throws IOException { 244 Path path = Paths.get("updateReadStdinWriteStdout.jar"); 245 246 for (String opts : new String[]{"u", "-u", "--update"}) { 247 if (legacyOnly && opts.startsWith("--")) 248 continue; 249 250 createJar(path, RES1); 251 jarWithStdin(path.toFile(), opts, RES2) 252 .assertSuccess() 253 .resultChecker(r -> { 254 ASSERT_CONTAINS_RES1.accept(r.stdoutAsStream()); 255 ASSERT_CONTAINS_RES2.accept(r.stdoutAsStream()); 256 ASSERT_CONTAINS_MAINFEST.accept(r.stdoutAsStream()); 257 }); 258 } 259 FileUtils.deleteFileIfExistsWithRetry(path); 260 } 261 262 @Test updateReadStdinWriteStdoutNoManifest()263 public void updateReadStdinWriteStdoutNoManifest() throws IOException { 264 Path path = Paths.get("updateReadStdinWriteStdoutNoManifest.jar"); 265 266 for (String opts : new String[]{"uM", "-uM", "--update --no-manifest"} ){ 267 if (legacyOnly && opts.startsWith("--")) 268 continue; 269 270 createJar(path, RES1); 271 jarWithStdin(path.toFile(), opts, RES2) 272 .assertSuccess() 273 .resultChecker(r -> { 274 ASSERT_CONTAINS_RES1.accept(r.stdoutAsStream()); 275 ASSERT_CONTAINS_RES2.accept(r.stdoutAsStream()); 276 ASSERT_DOES_NOT_CONTAIN_MAINFEST.accept(r.stdoutAsStream()); 277 }); 278 } 279 FileUtils.deleteFileIfExistsWithRetry(path); 280 } 281 282 // List 283 284 @Test listBadArgs()285 public void listBadArgs() { 286 jar("tx") 287 .assertFailure() 288 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 289 290 jar("-tx") 291 .assertFailure() 292 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 293 294 if (!legacyOnly) 295 jar("--list --extract") 296 .assertFailure() 297 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 298 } 299 300 @Test listReadFromFileWriteToStdout()301 public void listReadFromFileWriteToStdout() throws IOException { 302 Path path = Paths.get("listReadFromFileWriteToStdout.jar"); // for listing 303 createJar(path, RES1); 304 String jn = path.toString(); 305 306 for (String opts : new String[]{"tf " + jn, "-tf " + jn, "--list --file " + jn}) { 307 if (legacyOnly && opts.startsWith("--")) 308 continue; 309 310 jar(opts) 311 .assertSuccess() 312 .resultChecker(r -> 313 assertTrue(r.output.contains("META-INF/MANIFEST.MF") && r.output.contains(RES1), 314 "Failed, got [" + r.output + "]") 315 ); 316 } 317 FileUtils.deleteFileIfExistsWithRetry(path); 318 } 319 320 @Test listReadFromStdinWriteToStdout()321 public void listReadFromStdinWriteToStdout() throws IOException { 322 Path path = Paths.get("listReadFromStdinWriteToStdout.jar"); 323 createJar(path, RES1); 324 325 for (String opts : new String[]{"t", "-t", "--list"} ){ 326 if (legacyOnly && opts.startsWith("--")) 327 continue; 328 329 jarWithStdin(path.toFile(), opts) 330 .assertSuccess() 331 .resultChecker(r -> 332 assertTrue(r.output.contains("META-INF/MANIFEST.MF") && r.output.contains(RES1), 333 "Failed, got [" + r.output + "]") 334 ); 335 } 336 FileUtils.deleteFileIfExistsWithRetry(path); 337 } 338 339 // Extract 340 341 @Test extractBadArgs()342 public void extractBadArgs() { 343 jar("xi") 344 .assertFailure() 345 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 346 347 jar("-xi") 348 .assertFailure() 349 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 350 351 if (!legacyOnly) { 352 jar("--extract --generate-index") 353 .assertFailure() 354 .resultChecker(new FailCheckerWithMessage( 355 "option --generate-index requires an argument")); 356 357 jar("--extract --generate-index=foo") 358 .assertFailure() 359 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 360 } 361 } 362 363 @Test extractReadFromStdin()364 public void extractReadFromStdin() throws IOException { 365 Path path = Paths.get("extract"); 366 Path jarPath = path.resolve("extractReadFromStdin.jar"); // for extracting 367 createJar(jarPath, RES1); 368 369 for (String opts : new String[]{"x" ,"-x", "--extract"}) { 370 if (legacyOnly && opts.startsWith("--")) 371 continue; 372 373 jarWithStdinAndWorkingDir(jarPath.toFile(), path.toFile(), opts) 374 .assertSuccess() 375 .resultChecker(r -> 376 assertTrue(Files.exists(path.resolve(RES1)), 377 "Expected to find:" + path.resolve(RES1)) 378 ); 379 FileUtils.deleteFileIfExistsWithRetry(path.resolve(RES1)); 380 } 381 FileUtils.deleteFileTreeWithRetry(path); 382 } 383 384 @Test extractReadFromFile()385 public void extractReadFromFile() throws IOException { 386 Path path = Paths.get("extract"); 387 String jn = "extractReadFromFile.jar"; 388 Path jarPath = path.resolve(jn); 389 createJar(jarPath, RES1); 390 391 for (String opts : new String[]{"xf "+jn ,"-xf "+jn, "--extract --file "+jn}) { 392 if (legacyOnly && opts.startsWith("--")) 393 continue; 394 395 jarWithStdinAndWorkingDir(null, path.toFile(), opts) 396 .assertSuccess() 397 .resultChecker(r -> 398 assertTrue(Files.exists(path.resolve(RES1)), 399 "Expected to find:" + path.resolve(RES1)) 400 ); 401 FileUtils.deleteFileIfExistsWithRetry(path.resolve(RES1)); 402 } 403 FileUtils.deleteFileTreeWithRetry(path); 404 } 405 406 // Basic help 407 408 @Test helpBadOptionalArg()409 public void helpBadOptionalArg() { 410 if (legacyOnly) 411 return; 412 413 jar("--help:") 414 .assertFailure(); 415 416 jar("--help:blah") 417 .assertFailure(); 418 } 419 420 @Test help()421 public void help() { 422 if (legacyOnly) 423 return; 424 425 jar("-h") 426 .assertSuccess() 427 .resultChecker(r -> 428 assertTrue(r.output.startsWith("Usage: jar [OPTION...] [ [--release VERSION] [-C dir] files]"), 429 "Failed, got [" + r.output + "]") 430 ); 431 432 jar("--help") 433 .assertSuccess() 434 .resultChecker(r -> { 435 assertTrue(r.output.startsWith("Usage: jar [OPTION...] [ [--release VERSION] [-C dir] files]"), 436 "Failed, got [" + r.output + "]"); 437 assertFalse(r.output.contains("--do-not-resolve-by-default")); 438 assertFalse(r.output.contains("--warn-if-resolved")); 439 }); 440 441 jar("--help:compat") 442 .assertSuccess() 443 .resultChecker(r -> 444 assertTrue(r.output.startsWith("Compatibility Interface:"), 445 "Failed, got [" + r.output + "]") 446 ); 447 448 jar("--help-extra") 449 .assertSuccess() 450 .resultChecker(r -> { 451 assertTrue(r.output.startsWith("Usage: jar [OPTION...] [ [--release VERSION] [-C dir] files]"), 452 "Failed, got [" + r.output + "]"); 453 assertTrue(r.output.contains("--do-not-resolve-by-default")); 454 assertTrue(r.output.contains("--warn-if-resolved")); 455 }); 456 } 457 458 // -- Infrastructure 459 jarContains(JarInputStream jis, String entryName)460 static boolean jarContains(JarInputStream jis, String entryName) 461 throws IOException 462 { 463 JarEntry e; 464 boolean found = false; 465 while((e = jis.getNextJarEntry()) != null) { 466 if (e.getName().equals(entryName)) 467 return true; 468 } 469 return false; 470 } 471 472 /* Creates a simple jar with entries of size 0, good enough for testing */ createJar(Path path, String... entries)473 static void createJar(Path path, String... entries) throws IOException { 474 FileUtils.deleteFileIfExistsWithRetry(path); 475 Path parent = path.getParent(); 476 if (parent != null) 477 Files.createDirectories(parent); 478 try (OutputStream out = Files.newOutputStream(path); 479 JarOutputStream jos = new JarOutputStream(out)) { 480 JarEntry je = new JarEntry("META-INF/MANIFEST.MF"); 481 jos.putNextEntry(je); 482 jos.closeEntry(); 483 484 for (String entry : entries) { 485 je = new JarEntry(entry); 486 jos.putNextEntry(je); 487 jos.closeEntry(); 488 } 489 } 490 } 491 492 static class FailCheckerWithMessage implements Consumer<Result> { 493 final String[] messages; FailCheckerWithMessage(String... m)494 FailCheckerWithMessage(String... m) { 495 messages = m; 496 } 497 @Override accept(Result r)498 public void accept(Result r) { 499 //out.printf("%s%n", r.output); 500 boolean found = false; 501 for (String m : messages) { 502 if (r.output.contains(m)) { 503 found = true; 504 break; 505 } 506 } 507 assertTrue(found, 508 "Excepted out to contain one of: " + Arrays.asList(messages) 509 + " but got: " + r.output); 510 } 511 } 512 jar(String... args)513 static Result jar(String... args) { 514 return jarWithStdinAndWorkingDir(null, null, args); 515 } 516 jarWithStdin(File stdinSource, String... args)517 static Result jarWithStdin(File stdinSource, String... args) { 518 return jarWithStdinAndWorkingDir(stdinSource, null, args); 519 } 520 jarWithStdinAndWorkingDir(File stdinFrom, File workingDir, String... args)521 static Result jarWithStdinAndWorkingDir(File stdinFrom, 522 File workingDir, 523 String... args) { 524 String jar = getJDKTool("jar"); 525 List<String> commands = new ArrayList<>(); 526 commands.add(jar); 527 if (!TOOL_VM_OPTIONS.isEmpty()) { 528 commands.addAll(Arrays.asList(TOOL_VM_OPTIONS.split("\\s+", -1))); 529 } 530 Stream.of(args).map(s -> s.split(" ")) 531 .flatMap(Arrays::stream) 532 .forEach(x -> commands.add(x)); 533 ProcessBuilder p = new ProcessBuilder(commands); 534 if (stdinFrom != null) 535 p.redirectInput(stdinFrom); 536 if (workingDir != null) 537 p.directory(workingDir); 538 return run(p); 539 } 540 run(ProcessBuilder pb)541 static Result run(ProcessBuilder pb) { 542 Process p; 543 byte[] stdout, stderr; 544 out.printf("Running: %s%n", pb.command()); 545 try { 546 p = pb.start(); 547 } catch (IOException e) { 548 throw new RuntimeException( 549 format("Couldn't start process '%s'", pb.command()), e); 550 } 551 552 String output; 553 try { 554 stdout = readAllBytes(p.getInputStream()); 555 stderr = readAllBytes(p.getErrorStream()); 556 557 output = toString(stdout, stderr); 558 } catch (IOException e) { 559 throw new RuntimeException( 560 format("Couldn't read process output '%s'", pb.command()), e); 561 } 562 563 try { 564 p.waitFor(); 565 } catch (InterruptedException e) { 566 throw new RuntimeException( 567 format("Process hasn't finished '%s'", pb.command()), e); 568 } 569 return new Result(p.exitValue(), stdout, stderr, output); 570 } 571 572 static final Path JAVA_HOME = Paths.get(System.getProperty("java.home")); 573 getJDKTool(String name)574 static String getJDKTool(String name) { 575 try { 576 return JDKToolFinder.getJDKTool(name); 577 } catch (Exception x) { 578 Path j = JAVA_HOME.resolve("bin").resolve(name); 579 if (Files.exists(j)) 580 return j.toString(); 581 j = JAVA_HOME.resolve("..").resolve("bin").resolve(name); 582 if (Files.exists(j)) 583 return j.toString(); 584 throw new RuntimeException(x); 585 } 586 } 587 toString(byte[] ba1, byte[] ba2)588 static String toString(byte[] ba1, byte[] ba2) { 589 return (new String(ba1, UTF_8)).concat(new String(ba2, UTF_8)); 590 } 591 592 static class Result { 593 final int exitValue; 594 final byte[] stdout; 595 final byte[] stderr; 596 final String output; 597 Result(int exitValue, byte[] stdout, byte[] stderr, String output)598 private Result(int exitValue, byte[] stdout, byte[] stderr, String output) { 599 this.exitValue = exitValue; 600 this.stdout = stdout; 601 this.stderr = stderr; 602 this.output = output; 603 } 604 stdoutAsStream()605 InputStream stdoutAsStream() { return new ByteArrayInputStream(stdout); } 606 assertSuccess()607 Result assertSuccess() { assertTrue(exitValue == 0, output); return this; } assertFailure()608 Result assertFailure() { assertTrue(exitValue != 0, output); return this; } 609 resultChecker(IOConsumer<Result> r)610 Result resultChecker(IOConsumer<Result> r) { 611 try { r.accept(this); return this; } 612 catch (IOException x) { throw new UncheckedIOException(x); } 613 } 614 resultChecker(FailCheckerWithMessage c)615 Result resultChecker(FailCheckerWithMessage c) { c.accept(this); return this; } 616 } 617 accept(T t)618 interface IOConsumer<T> { void accept(T t) throws IOException ; } 619 620 // readAllBytes implementation so the test can be run pre 1.9 ( legacyOnly ) readAllBytes(InputStream is)621 static byte[] readAllBytes(InputStream is) throws IOException { 622 byte[] buf = new byte[8192]; 623 int capacity = buf.length; 624 int nread = 0; 625 int n; 626 for (;;) { 627 // read to EOF which may read more or less than initial buffer size 628 while ((n = is.read(buf, nread, capacity - nread)) > 0) 629 nread += n; 630 631 // if the last call to read returned -1, then we're done 632 if (n < 0) 633 break; 634 635 // need to allocate a larger buffer 636 capacity = capacity << 1; 637 638 buf = Arrays.copyOf(buf, capacity); 639 } 640 return (capacity == nread) ? buf : Arrays.copyOf(buf, nread); 641 } 642 643 // Standalone entry point for running with, possibly older, JDKs. main(String[] args)644 public static void main(String[] args) throws Throwable { 645 boolean legacyOnly = false; 646 if (args.length != 0 && args[0].equals("legacyOnly")) 647 legacyOnly = true; 648 649 CLICompatibility test = new CLICompatibility(legacyOnly); 650 for (Method m : CLICompatibility.class.getDeclaredMethods()) { 651 if (m.getAnnotation(Test.class) != null) { 652 System.out.println("Invoking " + m.getName()); 653 m.invoke(test); 654 } 655 } 656 } CLICompatibility(boolean legacyOnly)657 CLICompatibility(boolean legacyOnly) { this.legacyOnly = legacyOnly; } CLICompatibility()658 CLICompatibility() { this.legacyOnly = false; } 659 } 660