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 }