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                 Path jarFile = self.inputDir().resolve(appDesc.jarFileName());
 141                 createJarBuilder().setOutputJar(jarFile).addSourceFile(
 142                         HELLO_JAVA).create();
 143             });
 144         } else if (appDesc.jmodFileName() != null) {
 145             // Modular app in .jmod file
 146             cmd.addPrerequisiteAction(unused -> {
 147                 createBundle(appDesc, getModulePath.get());
 148             });
 149         } else {
 150             // Modular app in .jar file
 151             cmd.addPrerequisiteAction(unused -> {
 152                 final Path jarFile;
 153                 if (moduleName == null) {
 154                     jarFile = cmd.inputDir().resolve(appDesc.jarFileName());
 155                 } else {
 156                     jarFile = getModulePath.get().resolve(appDesc.jarFileName());
 157                 }
 158 
 159                 TKit.withTempDirectory("src",
 160                         workDir -> prepareSources(workDir).setOutputJar(jarFile).create());
 161             });
 162         }
 163 
 164         if (moduleName == null) {
 165             cmd.addArguments("--main-jar", appDesc.jarFileName());
 166             cmd.addArguments("--main-class", qualifiedClassName);
 167         } else {
 168             cmd.addArguments("--module-path", TKit.workDir().resolve(
 169                     "input-modules"));
 170             cmd.addArguments("--module", String.join("/", moduleName,
 171                     qualifiedClassName));
 172             // For modular app assume nothing will go in input directory and thus
 173             // nobody will create input directory, so remove corresponding option
 174             // from jpackage command line.
 175             cmd.removeArgumentWithValue("--input");
 176         }
 177         if (TKit.isWindows()) {
 178             cmd.addArguments("--win-console");
 179         }
 180     }
 181 
 182     static JavaAppDesc createDefaltAppDesc() {
 183         return new JavaAppDesc().setClassName(CLASS_NAME).setBundleFileName("hello.jar");
 184     }
 185 
 186     static void verifyOutputFile(Path outputFile, List<String> args,
 187             Map<String, String> params) {
 188         if (!outputFile.isAbsolute()) {
 189             verifyOutputFile(outputFile.toAbsolutePath().normalize(), args,
 190                     params);
 191             return;
 192         }
 193 
 194         TKit.assertFileExists(outputFile);
 195 
 196         List<String> contents = ThrowingSupplier.toSupplier(
 197                 () -> Files.readAllLines(outputFile)).get();
 198 
 199         List<String> expected = new ArrayList<>(List.of(
 200                 "jpackage test application",
 201                 String.format("args.length: %d", args.size())
 202         ));
 203         expected.addAll(args);
 204         expected.addAll(params.entrySet().stream()
 205                 .sorted(Comparator.comparing(Map.Entry::getKey))
 206                 .map(entry -> String.format("-D%s=%s", entry.getKey(),
 207                         entry.getValue()))
 208                 .collect(Collectors.toList()));
 209 
 210         TKit.assertStringListEquals(expected, contents, String.format(
 211                 "Check contents of [%s] file", outputFile));
 212     }
 213 
 214     public static Path createBundle(JavaAppDesc appDesc, Path outputDir) {
 215         String jmodFileName = appDesc.jmodFileName();
 216         if (jmodFileName != null) {
 217             final Path jmodFilePath = outputDir.resolve(jmodFileName);
 218             TKit.withTempDirectory("jmod-workdir", jmodWorkDir -> {
 219                 var jarAppDesc = JavaAppDesc.parse(appDesc.toString())
 220                         .setBundleFileName("tmp.jar");
 221                 Path jarPath = createBundle(jarAppDesc, jmodWorkDir);
 222                 Executor exec = new Executor()
 223                         .setToolProvider(JavaTool.JMOD)
 224                         .addArguments("create", "--class-path")
 225                         .addArgument(jarPath)
 226                         .addArgument(jmodFilePath);
 227 
 228                 if (appDesc.isWithMainClass()) {
 229                     exec.addArguments("--main-class", appDesc.className());
 230                 }
 231 
 232                 if (appDesc.moduleVersion() != null) {
 233                     exec.addArguments("--module-version", appDesc.moduleVersion());
 234                 }
 235 
 236                 Files.createDirectories(jmodFilePath.getParent());
 237                 exec.execute();
 238             });
 239 
 240             return jmodFilePath;
 241         }
 242 
 243         final JavaAppDesc jarAppDesc;
 244         if (appDesc.isWithBundleFileName()) {
 245             jarAppDesc = appDesc;
 246         } else {
 247             // Create copy of original JavaAppDesc instance.
 248             jarAppDesc = JavaAppDesc.parse(appDesc.toString())
 249                         .setBundleFileName(createDefaltAppDesc().jarFileName());
 250         }
 251 
 252         JPackageCommand
 253                 .helloAppImage(jarAppDesc)
 254                 .setArgumentValue("--input", outputDir)
 255                 .setArgumentValue("--module-path", outputDir)
 256                 .executePrerequisiteActions();
 257 
 258         return outputDir.resolve(jarAppDesc.jarFileName());
 259     }
 260 
 261     public static void executeLauncherAndVerifyOutput(JPackageCommand cmd,
 262             String... args) {
 263         final Path launcherPath = cmd.appLauncherPath();
 264         if (cmd.isFakeRuntime(String.format("Not running [%s] launcher",
 265                 launcherPath))) {
 266             return;
 267         }
 268 
 269         assertApp(launcherPath)
 270         .addDefaultArguments(Optional
 271                 .ofNullable(cmd.getAllArgumentValues("--arguments"))
 272                 .orElseGet(() -> new String[0]))
 273         .addJavaOptions(Optional
 274                 .ofNullable(cmd.getAllArgumentValues("--java-options"))
 275                 .orElseGet(() -> new String[0]))
 276         .executeAndVerifyOutput(args);
 277     }
 278 
 279     public final static class AppOutputVerifier {
 280         AppOutputVerifier(Path helloAppLauncher) {
 281             this.launcherPath = helloAppLauncher;
 282             this.params = new HashMap<>();
 283             this.defaultLauncherArgs = new ArrayList<>();
 284         }
 285 
 286         public AppOutputVerifier addDefaultArguments(String... v) {
 287             return addDefaultArguments(List.of(v));
 288         }
 289 
 290         public AppOutputVerifier addDefaultArguments(Collection<String> v) {
 291             defaultLauncherArgs.addAll(v);
 292             return this;
 293         }
 294 
 295         public AppOutputVerifier addParam(String name, String value) {
 296             if (name.startsWith("param")) {
 297                 params.put(name, value);
 298             }
 299             return this;
 300         }
 301 
 302         public AppOutputVerifier addParams(Collection<Map.Entry<String, String>> v) {
 303             v.forEach(entry -> addParam(entry.getKey(), entry.getValue()));
 304             return this;
 305         }
 306         public AppOutputVerifier addParams(Map<String, String> v) {
 307             return addParams(v.entrySet());
 308         }
 309 
 310         public AppOutputVerifier addParams(Map.Entry<String, String>... v) {
 311             return addParams(List.of(v));
 312         }
 313 
 314         public AppOutputVerifier addJavaOptions(String... v) {
 315             return addJavaOptions(List.of(v));
 316         }
 317 
 318         public AppOutputVerifier addJavaOptions(Collection<String> v) {
 319             return addParams(v.stream()
 320             .filter(javaOpt -> javaOpt.startsWith("-D"))
 321             .map(javaOpt -> {
 322                 var components = javaOpt.split("=", 2);
 323                 return Map.entry(components[0].substring(2), components[1]);
 324             })
 325             .collect(Collectors.toList()));
 326         }
 327 
 328         public void executeAndVerifyOutput(String... args) {
 329             // Output file will be created in the current directory.
 330             Path outputFile = TKit.workDir().resolve(OUTPUT_FILENAME);
 331             ThrowingFunction.toFunction(Files::deleteIfExists).apply(outputFile);
 332 
 333             final Path executablePath;
 334             if (launcherPath.isAbsolute()) {
 335                 executablePath = launcherPath;
 336             } else {
 337                 // Make sure path to executable is relative to the current directory.
 338                 executablePath = Path.of(".").resolve(launcherPath.normalize());
 339             }
 340 
 341             final List<String> launcherArgs = List.of(args);
 342             new Executor()
 343                     .setDirectory(outputFile.getParent())
 344                     .setExecutable(executablePath)
 345                     .addArguments(launcherArgs)
 346                     .dumpOutput()
 347                     .execute();
 348 
 349             final List<String> appArgs;
 350             if (launcherArgs.isEmpty()) {
 351                 appArgs = defaultLauncherArgs;
 352             } else {
 353                 appArgs = launcherArgs;
 354             }
 355 
 356             verifyOutputFile(outputFile, appArgs, params);
 357         }
 358 
 359         private final Path launcherPath;
 360         private final List<String> defaultLauncherArgs;
 361         private final Map<String, String> params;
 362     }
 363 
 364     public static AppOutputVerifier assertApp(Path helloAppLauncher) {
 365         return new AppOutputVerifier(helloAppLauncher);
 366     }
 367 
 368     final static String OUTPUT_FILENAME = "appOutput.txt";
 369 
 370     private final JavaAppDesc appDesc;
 371 
 372     private static final Path HELLO_JAVA = TKit.TEST_SRC_ROOT.resolve(
 373             "apps/image/Hello.java");
 374 
 375     private final static String CLASS_NAME = HELLO_JAVA.getFileName().toString().split(
 376             "\\.", 2)[0];
 377 }