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 24 package jdk.jpackage.tests; 25 26 import java.io.IOException; 27 import java.nio.file.Files; 28 import java.util.Collection; 29 import java.util.ArrayList; 30 import java.util.List; 31 import java.util.Set; 32 import java.util.jar.JarFile; 33 import java.util.Objects; 34 import java.util.stream.Collectors; 35 import java.util.stream.Stream; 36 import java.nio.file.Path; 37 import java.util.function.Predicate; 38 import java.util.jar.JarEntry; 39 import jdk.jpackage.test.Annotations.Parameters; 40 import jdk.jpackage.test.Annotations.Test; 41 import jdk.jpackage.test.*; 42 import jdk.jpackage.test.Functional.ThrowingConsumer; 43 import static jdk.jpackage.tests.MainClassTest.Script.MainClassType.*; 44 45 46 /* 47 * @test 48 * @summary test different settings of main class name for jpackage 49 * @library ../../../../helpers 50 * @build jdk.jpackage.test.* 51 * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal 52 * @compile MainClassTest.java 53 * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main 54 * --jpt-run=jdk.jpackage.tests.MainClassTest 55 */ 56 57 public final class MainClassTest { 58 59 static final class Script { 60 Script() { 61 appDesc = JavaAppDesc.parse("test.Hello"); 62 } 63 64 Script modular(boolean v) { 65 appDesc.setModuleName(v ? "com.other" : null); 66 return this; 67 } 68 69 Script withJLink(boolean v) { 70 withJLink = v; 71 return this; 72 } 73 74 Script withMainClass(MainClassType v) { 75 mainClass = v; 76 return this; 77 } 78 79 Script withJarMainClass(MainClassType v) { 80 appDesc.setJarWithMainClass(v != NotSet); 81 jarMainClass = v; 82 return this; 83 } 84 85 Script expectedErrorMessage(String v) { 86 expectedErrorMessage = v; 87 return this; 88 } 89 90 @Override 91 public String toString() { 92 return Stream.of( 93 format("modular", appDesc.moduleName() != null ? 'y' : 'n'), 94 format("main-class", mainClass), 95 format("jar-main-class", jarMainClass), 96 format("jlink", withJLink ? 'y' : 'n'), 97 format("error", expectedErrorMessage) 98 ).filter(Objects::nonNull).collect(Collectors.joining("; ")); 99 } 100 101 private static String format(String key, Object value) { 102 if (value == null) { 103 return null; 104 } 105 return String.join("=", key, value.toString()); 106 } 107 108 enum MainClassType { 109 NotSet("n"), 110 SetWrong("b"), 111 SetRight("y"); 112 113 MainClassType(String label) { 114 this.label = label; 115 } 116 117 @Override 118 public String toString() { 119 return label; 120 } 121 122 private final String label; 123 }; 124 125 private JavaAppDesc appDesc; 126 private boolean withJLink; 127 private MainClassType mainClass; 128 private MainClassType jarMainClass; 129 private String expectedErrorMessage; 130 } 131 132 public MainClassTest(Script script) { 133 this.script = script; 134 135 nonExistingMainClass = Stream.of( 136 script.appDesc.packageName(), "ThereIsNoSuchClass").filter( 137 Objects::nonNull).collect(Collectors.joining(".")); 138 139 cmd = JPackageCommand 140 .helloAppImage(script.appDesc) 141 .ignoreDefaultRuntime(true); 142 if (!script.withJLink) { 143 cmd.addArguments("--runtime-image", Path.of(System.getProperty( 144 "java.home"))); 145 } 146 147 final String moduleName = script.appDesc.moduleName(); 148 switch (script.mainClass) { 149 case NotSet: 150 if (moduleName != null) { 151 // Don't specify class name, only module name. 152 cmd.setArgumentValue("--module", moduleName); 153 } else { 154 cmd.removeArgumentWithValue("--main-class"); 155 } 156 break; 157 158 case SetWrong: 159 if (moduleName != null) { 160 cmd.setArgumentValue("--module", 161 String.join("/", moduleName, nonExistingMainClass)); 162 } else { 163 cmd.setArgumentValue("--main-class", nonExistingMainClass); 164 } 165 } 166 } 167 168 @Parameters 169 public static Collection scripts() { 170 final var withMainClass = Set.of(SetWrong, SetRight); 171 172 List<Script[]> scripts = new ArrayList<>(); 173 for (var withJLink : List.of(true, false)) { 174 for (var modular : List.of(true, false)) { 175 for (var mainClass : Script.MainClassType.values()) { 176 for (var jarMainClass : Script.MainClassType.values()) { 177 Script script = new Script() 178 .modular(modular) 179 .withJLink(withJLink) 180 .withMainClass(mainClass) 181 .withJarMainClass(jarMainClass); 182 183 if (withMainClass.contains(jarMainClass) 184 || withMainClass.contains(mainClass)) { 185 } else if (modular) { 186 script.expectedErrorMessage( 187 "Error: Main application class is missing"); 188 } else { 189 script.expectedErrorMessage( 190 "A main class was not specified nor was one found in the jar"); 191 } 192 193 scripts.add(new Script[]{script}); 194 } 195 } 196 } 197 } 198 return scripts; 199 } 200 201 @Test 202 public void test() throws IOException { 203 if (script.jarMainClass == SetWrong) { 204 initJarWithWrongMainClass(); 205 } 206 207 if (script.expectedErrorMessage != null) { 208 // This is the case when main class is not found nor in jar 209 // file nor on command line. 210 List<String> output = cmd 211 .saveConsoleOutput(true) 212 .execute() 213 .assertExitCodeIs(1) 214 .getOutput(); 215 TKit.assertTextStream(script.expectedErrorMessage).apply(output.stream()); 216 return; 217 } 218 219 // Get here only if main class is specified. 220 boolean appShouldSucceed = false; 221 222 // Should succeed if valid main class is set on the command line. 223 appShouldSucceed |= (script.mainClass == SetRight); 224 225 // Should succeed if main class is not set on the command line but set 226 // to valid value in the jar. 227 appShouldSucceed |= (script.mainClass == NotSet && script.jarMainClass == SetRight); 228 229 if (appShouldSucceed) { 230 cmd.executeAndAssertHelloAppImageCreated(); 231 } else { 232 cmd.executeAndAssertImageCreated(); 233 if (!cmd.isFakeRuntime(String.format("Not running [%s]", 234 cmd.appLauncherPath()))) { 235 List<String> output = new Executor() 236 .setDirectory(cmd.outputDir()) 237 .setExecutable(cmd.appLauncherPath()) 238 .dumpOutput().saveOutput() 239 .execute().assertExitCodeIs(1).getOutput(); 240 TKit.assertTextStream(String.format( 241 "Error: Could not find or load main class %s", 242 nonExistingMainClass)).apply(output.stream()); 243 } 244 } 245 } 246 247 private void initJarWithWrongMainClass() throws IOException { 248 // Call JPackageCommand.executePrerequisiteActions() to build app's jar. 249 // executePrerequisiteActions() is called by JPackageCommand instance 250 // only once. 251 cmd.executePrerequisiteActions(); 252 253 final Path jarFile; 254 if (script.appDesc.moduleName() != null) { 255 jarFile = Path.of(cmd.getArgumentValue("--module-path"), 256 script.appDesc.jarFileName()); 257 } else { 258 jarFile = cmd.inputDir().resolve(cmd.getArgumentValue("--main-jar")); 259 } 260 261 // Create new jar file filtering out main class from the old jar file. 262 TKit.withTempDirectory("repack-jar", workDir -> { 263 // Extract app's class from the old jar. 264 explodeJar(jarFile, workDir, 265 jarEntry -> Path.of(jarEntry.getName()).equals( 266 script.appDesc.classFilePath())); 267 268 // Create app's jar file with different main class. 269 var badAppDesc = JavaAppDesc.parse(script.appDesc.toString()).setClassName( 270 nonExistingMainClass); 271 JPackageCommand.helloAppImage(badAppDesc).executePrerequisiteActions(); 272 273 // Extract new jar but skip app's class. 274 explodeJar(jarFile, workDir, 275 jarEntry -> !Path.of(jarEntry.getName()).equals( 276 badAppDesc.classFilePath())); 277 278 // At this point we should have: 279 // 1. Manifest from the new jar referencing non-existing class 280 // as the main class. 281 // 2. Module descriptor referencing non-existing class as the main 282 // class in case of modular app. 283 // 3. App's class from the old jar. We need it to let jlink find some 284 // classes in the package declared in module descriptor 285 // in case of modular app. 286 287 Files.delete(jarFile); 288 new Executor().setToolProvider(JavaTool.JAR) 289 .addArguments("-v", "-c", "-M", "-f", jarFile.toString()) 290 .addArguments("-C", workDir.toString(), ".") 291 .dumpOutput() 292 .execute().assertExitCodeIsZero(); 293 }); 294 } 295 296 private static void explodeJar(Path jarFile, Path workDir, 297 Predicate<JarEntry> filter) throws IOException { 298 try (var jar = new JarFile(jarFile.toFile())) { 299 jar.stream() 300 .filter(Predicate.not(JarEntry::isDirectory)) 301 .filter(filter) 302 .sequential().forEachOrdered(ThrowingConsumer.toConsumer( 303 jarEntry -> { 304 try (var in = jar.getInputStream(jarEntry)) { 305 Path fileName = workDir.resolve(jarEntry.getName()); 306 Files.createDirectories(fileName.getParent()); 307 Files.copy(in, fileName); 308 } 309 })); 310 } 311 } 312 313 private final JPackageCommand cmd; 314 private final Script script; 315 private final String nonExistingMainClass; 316 }