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 }