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 }