1 /*
   2  * Copyright (c) 2019, 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 package jdk.jpackage.test;
  24 
  25 import java.io.File;
  26 import java.io.IOException;
  27 import java.nio.file.Files;
  28 import java.nio.file.Path;
  29 import java.util.*;
  30 import java.util.concurrent.atomic.AtomicBoolean;
  31 import java.util.function.Supplier;
  32 import java.util.regex.Matcher;
  33 import java.util.regex.Pattern;
  34 import java.util.stream.Collectors;
  35 import jdk.jpackage.test.Functional.ThrowingFunction;
  36 import jdk.jpackage.test.Functional.ThrowingSupplier;
  37 
  38 public final class HelloApp {
  39 
  40     HelloApp(JavaAppDesc appDesc) {
  41         if (appDesc == null) {
  42             this.appDesc = createDefaltAppDesc();
  43         } else {
  44             this.appDesc = appDesc;
  45         }
  46     }
  47 
  48     private JarBuilder prepareSources(Path srcDir) throws IOException {
  49         final String qualifiedClassName = appDesc.className();
  50 
  51         final String className = qualifiedClassName.substring(
  52                 qualifiedClassName.lastIndexOf('.') + 1);
  53         final String packageName = appDesc.packageName();
  54 
  55         final Path srcFile = srcDir.resolve(Path.of(String.join(
  56                 File.separator, qualifiedClassName.split("\\.")) + ".java"));
  57         Files.createDirectories(srcFile.getParent());
  58 
  59         JarBuilder jarBuilder = createJarBuilder().addSourceFile(srcFile);
  60         final String moduleName = appDesc.moduleName();
  61         if (moduleName != null) {
  62             Path moduleInfoFile = srcDir.resolve("module-info.java");
  63             TKit.createTextFile(moduleInfoFile, List.of(
  64                     String.format("module %s {", moduleName),
  65                     String.format("    exports %s;", packageName),
  66                     "    requires java.desktop;",
  67                     "}"
  68             ));
  69             jarBuilder.addSourceFile(moduleInfoFile);
  70             jarBuilder.setModuleVersion(appDesc.moduleVersion());
  71         }
  72 
  73         // Add package directive and replace class name in java source file.
  74         // Works with simple test Hello.java.
  75         // Don't expect too much from these regexps!
  76         Pattern classNameRegex = Pattern.compile("\\bHello\\b");
  77         Pattern classDeclaration = Pattern.compile(
  78                 "(^.*\\bclass\\s+)\\bHello\\b(.*$)");
  79         Pattern importDirective = Pattern.compile(
  80                 "(?<=import (?:static )?+)[^;]+");
  81         AtomicBoolean classDeclared = new AtomicBoolean();
  82         AtomicBoolean packageInserted = new AtomicBoolean(packageName == null);
  83 
  84         var packageInserter = Functional.identityFunction((line) -> {
  85             packageInserted.setPlain(true);
  86             return String.format("package %s;%s%s", packageName,
  87                     System.lineSeparator(), line);
  88         });
  89 
  90         Files.write(srcFile, Files.readAllLines(HELLO_JAVA).stream().map(line -> {
  91             Matcher m;
  92             if (classDeclared.getPlain()) {
  93                 if ((m = classNameRegex.matcher(line)).find()) {
  94                     line = m.replaceAll(className);
  95                 }
  96                 return line;
  97             }
  98 
  99             if (!packageInserted.getPlain() && importDirective.matcher(line).find()) {
 100                 line = packageInserter.apply(line);
 101             } else if ((m = classDeclaration.matcher(line)).find()) {
 102                 classDeclared.setPlain(true);
 103                 line = m.group(1) + className + m.group(2);
 104                 if (!packageInserted.getPlain()) {
 105                     line = packageInserter.apply(line);
 106                 }
 107             }
 108             return line;
 109         }).collect(Collectors.toList()));
 110 
 111         return jarBuilder;
 112     }
 113 
 114     private JarBuilder createJarBuilder() {
 115         JarBuilder builder = new JarBuilder();
 116         if (appDesc.isWithMainClass()) {
 117             builder.setMainClass(appDesc.className());
 118         }
 119         return builder;
 120     }
 121 
 122     void addTo(JPackageCommand cmd) {
 123         final String moduleName = appDesc.moduleName();
 124         final String qualifiedClassName = appDesc.className();
 125 
 126         if (moduleName != null && appDesc.packageName() == null) {
 127             throw new IllegalArgumentException(String.format(
 128                     "Module [%s] with default package", moduleName));
 129         }
 130 
 131         Supplier<Path> getModulePath = () -> {
 132             // `--module-path` option should be set by the moment
 133             // when this action is being executed.
 134             return cmd.getArgumentValue("--module-path", cmd::inputDir, Path::of);
 135         };
 136 
 137         if (moduleName == null && CLASS_NAME.equals(qualifiedClassName)) {
 138             // Use Hello.java as is.
 139             cmd.addPrerequisiteAction((self) -> {
 140                 if (self.inputDir() != null) {
 141                     Path jarFile = self.inputDir().resolve(appDesc.jarFileName());
 142                     createJarBuilder().setOutputJar(jarFile).addSourceFile(
 143                             HELLO_JAVA).create();
 144                 }
 145             });
 146         } else if (appDesc.jmodFileName() != null) {
 147             // Modular app in .jmod file
 148             cmd.addPrerequisiteAction(unused -> {
 149                 createBundle(appDesc, getModulePath.get());
 150             });
 151         } else {
 152             // Modular app in .jar file
 153             cmd.addPrerequisiteAction(unused -> {
 154                 final Path jarFile;
 155                 if (moduleName == null) {
 156                     jarFile = cmd.inputDir().resolve(appDesc.jarFileName());
 157                 } else if (getModulePath.get() != null) {
 158                     jarFile = getModulePath.get().resolve(appDesc.jarFileName());
 159                 } else {
 160                     jarFile = null;
 161                 }
 162                 if (jarFile != null) {
 163                     TKit.withTempDirectory("src",
 164                             workDir -> prepareSources(workDir).setOutputJar(jarFile).create());
 165                 }
 166             });
 167         }
 168 
 169         if (moduleName == null) {
 170             cmd.addArguments("--main-jar", appDesc.jarFileName());
 171             cmd.addArguments("--main-class", qualifiedClassName);
 172         } else {
 173             cmd.addArguments("--module-path", TKit.workDir().resolve(
 174                     "input-modules"));
 175             cmd.addArguments("--module", String.join("/", moduleName,
 176                     qualifiedClassName));
 177             // For modular app assume nothing will go in input directory and thus
 178             // nobody will create input directory, so remove corresponding option
 179             // from jpackage command line.
 180             cmd.removeArgumentWithValue("--input");
 181         }
 182         if (TKit.isWindows()) {
 183             cmd.addArguments("--win-console");
 184         }
 185     }
 186 
 187     static JavaAppDesc createDefaltAppDesc() {
 188         return new JavaAppDesc().setClassName(CLASS_NAME).setBundleFileName("hello.jar");
 189     }
 190 
 191     static void verifyOutputFile(Path outputFile, List<String> args,
 192             Map<String, String> params) {
 193         if (!outputFile.isAbsolute()) {
 194             verifyOutputFile(outputFile.toAbsolutePath().normalize(), args,
 195                     params);
 196             return;
 197         }
 198 
 199         TKit.assertFileExists(outputFile);
 200 
 201         List<String> contents = ThrowingSupplier.toSupplier(
 202                 () -> Files.readAllLines(outputFile)).get();
 203 
 204         List<String> expected = new ArrayList<>(List.of(
 205                 "jpackage test application",
 206                 String.format("args.length: %d", args.size())
 207         ));
 208         expected.addAll(args);
 209         expected.addAll(params.entrySet().stream()
 210                 .sorted(Comparator.comparing(Map.Entry::getKey))
 211                 .map(entry -> String.format("-D%s=%s", entry.getKey(),
 212                         entry.getValue()))
 213                 .collect(Collectors.toList()));
 214 
 215         TKit.assertStringListEquals(expected, contents, String.format(
 216                 "Check contents of [%s] file", outputFile));
 217     }
 218 
 219     public static Path createBundle(JavaAppDesc appDesc, Path outputDir) {
 220         String jmodFileName = appDesc.jmodFileName();
 221         if (jmodFileName != null) {
 222             final Path jmodFilePath = outputDir.resolve(jmodFileName);
 223             TKit.withTempDirectory("jmod-workdir", jmodWorkDir -> {
 224                 var jarAppDesc = JavaAppDesc.parse(appDesc.toString())
 225                         .setBundleFileName("tmp.jar");
 226                 Path jarPath = createBundle(jarAppDesc, jmodWorkDir);
 227                 Executor exec = new Executor()
 228                         .setToolProvider(JavaTool.JMOD)
 229                         .addArguments("create", "--class-path")
 230                         .addArgument(jarPath)
 231                         .addArgument(jmodFilePath);
 232 
 233                 if (appDesc.isWithMainClass()) {
 234                     exec.addArguments("--main-class", appDesc.className());
 235                 }
 236 
 237                 if (appDesc.moduleVersion() != null) {
 238                     exec.addArguments("--module-version", appDesc.moduleVersion());
 239                 }
 240 
 241                 Files.createDirectories(jmodFilePath.getParent());
 242                 exec.execute();
 243             });
 244 
 245             return jmodFilePath;
 246         }
 247 
 248         final JavaAppDesc jarAppDesc;
 249         if (appDesc.isWithBundleFileName()) {
 250             jarAppDesc = appDesc;
 251         } else {
 252             // Create copy of original JavaAppDesc instance.
 253             jarAppDesc = JavaAppDesc.parse(appDesc.toString())
 254                         .setBundleFileName(createDefaltAppDesc().jarFileName());
 255         }
 256 
 257         JPackageCommand
 258                 .helloAppImage(jarAppDesc)
 259                 .setArgumentValue("--input", outputDir)
 260                 .setArgumentValue("--module-path", outputDir)
 261                 .executePrerequisiteActions();
 262 
 263         return outputDir.resolve(jarAppDesc.jarFileName());
 264     }
 265 
 266     public static void executeLauncherAndVerifyOutput(JPackageCommand cmd,
 267             String... args) {
 268         AppOutputVerifier av = getVerifier(cmd, args);
 269         if (av != null) {
 270             av.executeAndVerifyOutput(args);
 271         }
 272     }
 273 
 274     public static Executor.Result executeLauncher(JPackageCommand cmd,
 275             String... args) {
 276         AppOutputVerifier av = getVerifier(cmd, args);
 277         return av.executeOnly(args);
 278     }
 279 
 280     private static AppOutputVerifier getVerifier(JPackageCommand cmd,
 281             String... args) {
 282         final Path launcherPath = cmd.appLauncherPath();
 283         if (cmd.isFakeRuntime(String.format("Not running [%s] launcher",
 284                 launcherPath))) {
 285             return null;
 286         }
 287 
 288         return assertApp(launcherPath)
 289         .addDefaultArguments(Optional
 290                 .ofNullable(cmd.getAllArgumentValues("--arguments"))
 291                 .orElseGet(() -> new String[0]))
 292         .addJavaOptions(Optional
 293                 .ofNullable(cmd.getAllArgumentValues("--java-options"))
 294                 .orElseGet(() -> new String[0]));
 295     }
 296 
 297 
 298     public final static class AppOutputVerifier {
 299         AppOutputVerifier(Path helloAppLauncher) {
 300             this.launcherPath = helloAppLauncher;
 301             this.params = new HashMap<>();
 302             this.defaultLauncherArgs = new ArrayList<>();
 303         }
 304 
 305         public AppOutputVerifier addDefaultArguments(String... v) {
 306             return addDefaultArguments(List.of(v));
 307         }
 308 
 309         public AppOutputVerifier addDefaultArguments(Collection<String> v) {
 310             defaultLauncherArgs.addAll(v);
 311             return this;
 312         }
 313 
 314         public AppOutputVerifier addParam(String name, String value) {
 315             if (name.startsWith("param")) {
 316                 params.put(name, value);
 317             }
 318             return this;
 319         }
 320 
 321         public AppOutputVerifier addParams(Collection<Map.Entry<String, String>> v) {
 322             v.forEach(entry -> addParam(entry.getKey(), entry.getValue()));
 323             return this;
 324         }
 325         public AppOutputVerifier addParams(Map<String, String> v) {
 326             return addParams(v.entrySet());
 327         }
 328 
 329         public AppOutputVerifier addParams(Map.Entry<String, String>... v) {
 330             return addParams(List.of(v));
 331         }
 332 
 333         public AppOutputVerifier addJavaOptions(String... v) {
 334             return addJavaOptions(List.of(v));
 335         }
 336 
 337         public AppOutputVerifier addJavaOptions(Collection<String> v) {
 338             return addParams(v.stream()
 339             .filter(javaOpt -> javaOpt.startsWith("-D"))
 340             .map(javaOpt -> {
 341                 var components = javaOpt.split("=", 2);
 342                 return Map.entry(components[0].substring(2), components[1]);
 343             })
 344             .collect(Collectors.toList()));
 345         }
 346 
 347         public void executeAndVerifyOutput(String... args) {
 348             getExecutor(args).dumpOutput().execute();
 349 
 350             final List<String> launcherArgs = List.of(args);
 351             final List<String> appArgs;
 352             if (launcherArgs.isEmpty()) {
 353                 appArgs = defaultLauncherArgs;
 354             } else {
 355                 appArgs = launcherArgs;
 356             }
 357 
 358             Path outputFile = TKit.workDir().resolve(OUTPUT_FILENAME);
 359             verifyOutputFile(outputFile, appArgs, params);
 360         }
 361 
 362         public Executor.Result executeOnly(String...args) {
 363             return getExecutor(args).saveOutput().executeWithoutExitCodeCheck();
 364         }
 365 
 366         private Executor getExecutor(String...args) {
 367 
 368             // Output file might be created in the current directory.
 369             Path outputFile = TKit.workDir().resolve(OUTPUT_FILENAME);
 370             ThrowingFunction.toFunction(Files::deleteIfExists).apply(outputFile);
 371 
 372             final Path executablePath;
 373             if (launcherPath.isAbsolute()) {
 374                 executablePath = launcherPath;
 375             } else {
 376                 // Make sure path to executable is relative to the current directory.
 377                 executablePath = Path.of(".").resolve(launcherPath.normalize());
 378             }
 379 
 380             final List<String> launcherArgs = List.of(args);
 381             return new Executor()
 382                     .setDirectory(outputFile.getParent())
 383                     .setExecutable(executablePath)
 384                     .addArguments(launcherArgs);
 385         }
 386 
 387         private final Path launcherPath;
 388         private final List<String> defaultLauncherArgs;
 389         private final Map<String, String> params;
 390     }
 391 
 392     public static AppOutputVerifier assertApp(Path helloAppLauncher) {
 393         return new AppOutputVerifier(helloAppLauncher);
 394     }
 395 
 396     final static String OUTPUT_FILENAME = "appOutput.txt";
 397 
 398     private final JavaAppDesc appDesc;
 399 
 400     private static final Path HELLO_JAVA = TKit.TEST_SRC_ROOT.resolve(
 401             "apps/image/Hello.java");
 402 
 403     private final static String CLASS_NAME = HELLO_JAVA.getFileName().toString().split(
 404             "\\.", 2)[0];
 405 }