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.nio.file.Path;
  26 import java.util.ArrayList;
  27 import java.util.Arrays;
  28 import java.util.Collection;
  29 import java.util.HashMap;
  30 import java.util.List;
  31 import java.util.ListIterator;
  32 import java.util.Map;
  33 import java.util.function.Function;
  34 import java.util.function.Supplier;
  35 import java.util.stream.Stream;
  36 
  37 /**
  38  * jpackage command line with prerequisite actions. Prerequisite actions can be
  39  * anything. The simplest is to compile test application and pack in a jar for
  40  * use on jpackage command line.
  41  */
  42 public final class JPackageCommand extends CommandArguments<JPackageCommand> {
  43 
  44     public JPackageCommand() {
  45         actions = new ArrayList<>();
  46     }
  47 
  48     static JPackageCommand createImmutable(JPackageCommand v) {
  49         JPackageCommand reply = new JPackageCommand();
  50         reply.immutable = true;
  51         reply.args.addAll(v.args);
  52         return reply;
  53     }
  54 
  55     public void setArgumentValue(String argName, String newValue) {
  56         verifyMutable();
  57 
  58         String prevArg = null;
  59         ListIterator<String> it = args.listIterator();
  60         while (it.hasNext()) {
  61             String value = it.next();
  62             if (prevArg != null && prevArg.equals(argName)) {
  63                 if (newValue != null) {
  64                     it.set(newValue);
  65                 } else {
  66                     it.remove();
  67                     it.previous();
  68                     it.remove();
  69                 }
  70                 return;
  71             }
  72             prevArg = value;
  73         }
  74 
  75         if (newValue != null) {
  76             addArguments(argName, newValue);
  77         }
  78     }
  79 
  80     public boolean hasArgument(String argName) {
  81         return args.contains(argName);
  82     }
  83 
  84     public <T> T getArgumentValue(String argName,
  85             Function<JPackageCommand, T> defaultValueSupplier,
  86             Function<String, T> stringConverter) {
  87         String prevArg = null;
  88         for (String arg : args) {
  89             if (prevArg != null && prevArg.equals(argName)) {
  90                 return stringConverter.apply(arg);
  91             }
  92             prevArg = arg;
  93         }
  94         if (defaultValueSupplier != null) {
  95             return defaultValueSupplier.apply(this);
  96         }
  97         return null;
  98     }
  99 
 100     public String getArgumentValue(String argName,
 101             Function<JPackageCommand, String> defaultValueSupplier) {
 102         return getArgumentValue(argName, defaultValueSupplier, v -> v);
 103     }
 104 
 105     public <T> T getArgumentValue(String argName,
 106             Supplier<T> defaultValueSupplier,
 107             Function<String, T> stringConverter) {
 108         return getArgumentValue(argName, (unused) -> defaultValueSupplier.get(),
 109                 stringConverter);
 110     }
 111 
 112     public String getArgumentValue(String argName,
 113             Supplier<String> defaultValueSupplier) {
 114         return getArgumentValue(argName, defaultValueSupplier, v -> v);
 115     }
 116 
 117     public String getArgumentValue(String argName) {
 118         return getArgumentValue(argName, (Supplier<String>)null);
 119     }
 120 
 121     public String[] getAllArgumentValues(String argName) {
 122         List<String> values = new ArrayList<>();
 123         String prevArg = null;
 124         for (String arg : args) {
 125             if (prevArg != null && prevArg.equals(argName)) {
 126                 values.add(arg);
 127             }
 128             prevArg = arg;
 129         }
 130         return values.toArray(String[]::new);
 131     }
 132 
 133     public PackageType packageType() {
 134         return getArgumentValue("--package-type",
 135                 () -> PackageType.DEFAULT,
 136                 (v) -> PACKAGE_TYPES.get(v));
 137     }
 138 
 139     public Path outputDir() {
 140         return getArgumentValue("--dest", () -> Test.defaultOutputDir(), Path::of);
 141     }
 142 
 143     public Path inputDir() {
 144         return getArgumentValue("--input", () -> Test.defaultInputDir(),Path::of);
 145     }
 146 
 147     public String version() {
 148         return getArgumentValue("--app-version", () -> "1.0");
 149     }
 150 
 151     public String name() {
 152         return getArgumentValue("--name", () -> getArgumentValue("--main-class"));
 153     }
 154 
 155     public boolean isRuntime() {
 156         return getArgumentValue("--runtime-image", () -> false, v -> true);
 157     }
 158 
 159     public JPackageCommand setDefaultInputOutput() {
 160         verifyMutable();
 161         addArguments("--input", Test.defaultInputDir().toString());
 162         addArguments("--dest", Test.defaultOutputDir().toString());
 163         return this;
 164     }
 165 
 166     JPackageCommand addAction(Runnable action) {
 167         verifyMutable();
 168         actions.add(action);
 169         return this;
 170     }
 171 
 172     public static JPackageCommand helloAppImage() {
 173         JPackageCommand cmd = new JPackageCommand();
 174         cmd.setDefaultInputOutput().setDefaultAppName();
 175         PackageType.IMAGE.applyTo(cmd);
 176         HelloApp.addTo(cmd);
 177         return cmd;
 178     }
 179 
 180     public JPackageCommand setPackageType(PackageType type) {
 181         verifyMutable();
 182         type.applyTo(this);
 183         return this;
 184     }
 185 
 186     JPackageCommand setDefaultAppName() {
 187         StackTraceElement st[] = Thread.currentThread().getStackTrace();
 188         for (StackTraceElement ste : st) {
 189             if ("main".equals(ste.getMethodName())) {
 190                 String name = ste.getClassName();
 191                 name = Stream.of(name.split("[.$]")).reduce((f, l) -> l).get();
 192                 addArguments("--name", name);
 193                 break;
 194             }
 195         }
 196         return this;
 197     }
 198 
 199     public Path outputBundle() {
 200         final PackageType type = packageType();
 201         if (PackageType.IMAGE == type) {
 202             return null;
 203         }
 204 
 205         String bundleName = null;
 206         if (PackageType.LINUX.contains(type)) {
 207             bundleName = LinuxHelper.getBundleName(this);
 208         } else if (PackageType.WINDOWS.contains(type)) {
 209             bundleName = WindowsHelper.getBundleName(this);
 210         } else if (PackageType.MAC.contains(type)) {
 211             bundleName = MacHelper.getBundleName(this);
 212         }
 213 
 214         return outputDir().resolve(bundleName);
 215     }
 216 
 217     /**
 218      * Returns path to directory where application will be installed.
 219      *
 220      * E.g. on Linux for app named Foo default the function will return
 221      * `/opt/foo`
 222      */
 223     public Path appInstallationDirectory() {
 224         final PackageType type = packageType();
 225         if (PackageType.IMAGE == type) {
 226             return null;
 227         }
 228 
 229         if (PackageType.LINUX.contains(type)) {
 230             // Launcher is in "bin" subfolder of the installation directory.
 231             return launcherInstallationPath().getParent().getParent();
 232         }
 233 
 234         if (PackageType.WINDOWS.contains(type)) {
 235             return WindowsHelper.getInstallationDirectory(this);
 236         }
 237 
 238         if (PackageType.MAC.contains(type)) {
 239             return MacHelper.getInstallationDirectory(this);
 240         }
 241 
 242         throw new IllegalArgumentException("Unexpected package type");
 243     }
 244 
 245     /**
 246      * Returns path where application launcher will be installed.
 247      * If the command will package Java run-time only, still returns path to
 248      * application launcher.
 249      *
 250      * E.g. on Linux for app named Foo default the function will return
 251      * `/opt/foo/bin/Foo`
 252      */
 253     public Path launcherInstallationPath() {
 254         final PackageType type = packageType();
 255         if (PackageType.IMAGE == type) {
 256             return null;
 257         }
 258 
 259         if (PackageType.LINUX.contains(type)) {
 260             return outputDir().resolve(LinuxHelper.getLauncherPath(this));
 261         }
 262 
 263         if (PackageType.WINDOWS.contains(type)) {
 264             return appInstallationDirectory().resolve(name() + ".exe");
 265         }
 266 
 267         if (PackageType.MAC.contains(type)) {
 268             return appInstallationDirectory().resolve(Path.of("Contents", "MacOS", name()));
 269         }
 270 
 271         throw new IllegalArgumentException("Unexpected package type");
 272     }
 273 
 274     /**
 275      * Returns path to application image directory.
 276      *
 277      * E.g. if --dest is set to `foo` and --name is set to `bar` the function
 278      * will return `foo/bar` path.
 279      *
 280      * @throws IllegalArgumentException is command is doing platform packaging
 281      */
 282     public Path appImage() {
 283         final PackageType type = packageType();
 284         if (PackageType.IMAGE != type) {
 285             throw new IllegalArgumentException("Unexpected package type");
 286         }
 287 
 288         return outputDir().resolve(name());
 289     }
 290 
 291     /**
 292      * Returns path to application launcher relative to image directory.
 293      *
 294      * E.g. if --name is set to `Foo` the function will return `bin/Foo` path on
 295      * Linux, and `Foo.exe` on Windows.
 296      *
 297      * @throws IllegalArgumentException is command is doing platform packaging
 298      */
 299     public Path launcherPathInAppImage() {
 300         final PackageType type = packageType();
 301         if (PackageType.IMAGE != type) {
 302             throw new IllegalArgumentException("Unexpected package type");
 303         }
 304 
 305         if (Test.isLinux()) {
 306             return Path.of("bin", name());
 307         }
 308 
 309         if (Test.isOSX()) {
 310             return Path.of("Contents", "MacOS", name());
 311         }
 312 
 313         if (Test.isWindows()) {
 314             return Path.of(name() + ".exe");
 315         }
 316 
 317         throw new IllegalArgumentException("Unexpected package type");
 318     }
 319 
 320     public Executor.Result execute() {
 321         verifyMutable();
 322         if (actions != null) {
 323             actions.stream().forEach(r -> r.run());
 324         }
 325         return new Executor()
 326                 .setExecutable(JavaTool.JPACKAGE)
 327                 .addArguments(args)
 328                 .execute();
 329     }
 330 
 331     String getPrintableCommandLine() {
 332         return new Executor()
 333                 .setExecutable(JavaTool.JPACKAGE)
 334                 .addArguments(args)
 335                 .getPrintableCommandLine();
 336     }
 337 
 338     void verifyIsOfType(Collection<PackageType> types) {
 339         verifyIsOfType(types.toArray(PackageType[]::new));
 340     }
 341 
 342     void verifyIsOfType(PackageType ... types) {
 343         if (!Arrays.asList(types).contains(packageType())) {
 344             throw new IllegalArgumentException("Unexpected package type");
 345         }
 346     }
 347 
 348     @Override
 349     protected boolean isMutable() {
 350         return !immutable;
 351     }
 352 
 353     private final List<Runnable> actions;
 354     private boolean immutable;
 355 
 356     private final static Map<String, PackageType> PACKAGE_TYPES
 357             = new Supplier<Map<String, PackageType>>() {
 358                 @Override
 359                 public Map<String, PackageType> get() {
 360                     Map<String, PackageType> reply = new HashMap<>();
 361                     for (PackageType type : PackageType.values()) {
 362                         reply.put(type.getName(), type);
 363                     }
 364                     return reply;
 365                 }
 366             }.get();
 367 }