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