1 /*
   2  * Copyright (c) 2011, 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.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.jpackage.internal;
  27 
  28 import java.io.File;
  29 import java.nio.file.Files;
  30 import java.nio.file.Path;
  31 import java.nio.file.InvalidPathException;
  32 import java.text.MessageFormat;
  33 import java.util.ArrayList;
  34 import java.util.Arrays;
  35 import java.util.Collection;
  36 import java.util.LinkedHashMap;
  37 import java.util.LinkedHashSet;
  38 import java.util.LinkedList;
  39 import java.util.List;
  40 import java.util.Map;
  41 import java.util.Set;
  42 import java.util.TreeMap;
  43 import java.util.TreeSet;
  44 
  45 /**
  46  * DeployParams
  47  *
  48  * This class is generated and used in Arguments.processArguments() as
  49  * intermediate step in generating the BundleParams and ultimately the Bundles
  50  */
  51 public class DeployParams {
  52 
  53     final List<RelativeFileSet> resources = new ArrayList<>();
  54 
  55     String id;
  56     String title;
  57     String vendor;
  58     String email;
  59     String description;
  60     String category;
  61     String licenseType;
  62     String copyright;
  63     String version;
  64     Boolean systemWide;
  65     Boolean serviceHint;
  66     Boolean signBundle;
  67     Boolean installdirChooser;
  68 
  69     String applicationClass;
  70 
  71     List<Param> params;
  72     List<String> arguments; //unnamed arguments
  73 
  74     // Java 9 modules support
  75     String addModules = null;
  76     String limitModules = null;
  77     Boolean stripNativeCommands = null;
  78     String modulePath = null;
  79     String module = null;
  80     String debugPort = null;
  81 
  82     File outdir = null;
  83 
  84     String appId = null;
  85 
  86     // list of jvm args
  87     // (in theory string can contain spaces and need to be escaped
  88     List<String> jvmargs = new LinkedList<>();
  89 
  90     // raw arguments to the bundler
  91     Map<String, ? super Object> bundlerArguments = new LinkedHashMap<>();
  92 
  93     void setCategory(String category) {
  94         this.category = category;
  95     }
  96 
  97     void setLicenseType(String licenseType) {
  98         this.licenseType = licenseType;
  99     }
 100 
 101     void setCopyright(String copyright) {
 102         this.copyright = copyright;
 103     }
 104 
 105     void setVersion(String version) {
 106         this.version = version;
 107     }
 108 
 109     void setSystemWide(Boolean systemWide) {
 110         this.systemWide = systemWide;
 111     }
 112 
 113     void setInstalldirChooser(Boolean installdirChooser) {
 114         this.installdirChooser = installdirChooser;
 115     }
 116 
 117     void setSignBundle(Boolean signBundle) {
 118         this.signBundle = signBundle;
 119     }
 120 
 121     void addJvmArg(String v) {
 122         jvmargs.add(v);
 123     }
 124 
 125     void setArguments(List<String> args) {
 126         this.arguments = args;
 127     }
 128 
 129     List<String> getArguments() {
 130         return this.arguments;
 131     }
 132 
 133     void addArgument(String arg) {
 134         this.arguments.add(arg);
 135     }
 136 
 137     void addAddModule(String value) {
 138         if (addModules == null) {
 139             addModules = value;
 140         }
 141         else {
 142             addModules += "," + value;
 143         }
 144     }
 145 
 146     void addLimitModule(String value) {
 147         if (limitModules == null) {
 148             limitModules = value;
 149         }
 150         else {
 151             limitModules += "," + value;
 152         }
 153     }
 154 
 155     String getModulePath() {
 156         return this.modulePath;
 157     }
 158 
 159     void setModulePath(String value) {
 160         this.modulePath = value;
 161     }
 162 
 163     void setModule(String value) {
 164         this.module = value;
 165     }
 166 
 167     void setDebug(String value) {
 168         this.debugPort = value;
 169     }
 170 
 171     void setStripNativeCommands(boolean value) {
 172         this.stripNativeCommands = value;
 173     }
 174 
 175     void setDescription(String description) {
 176         this.description = description;
 177     }
 178 
 179     public void setAppId(String id) {
 180         appId = id;
 181     }
 182 
 183     void setParams(List<Param> params) {
 184         this.params = params;
 185     }
 186 
 187     void setTitle(String title) {
 188         this.title = title;
 189     }
 190 
 191     void setVendor(String vendor) {
 192         this.vendor = vendor;
 193     }
 194 
 195     void setEmail(String email) {
 196         this.email = email;
 197     }
 198 
 199     void setApplicationClass(String applicationClass) {
 200         this.applicationClass = applicationClass;
 201     }
 202 
 203     File getOutput() {
 204         return outdir;
 205     }
 206 
 207     public void setOutput(File output) {
 208         outdir = output;
 209     }
 210 
 211     static class Template {
 212         File in;
 213         File out;
 214 
 215         Template(File in, File out) {
 216             this.in = in;
 217             this.out = out;
 218         }
 219     }
 220 
 221     // we need to expand as in some cases
 222     // (most notably jpackage)
 223     // we may get "." as filename and assumption is we include
 224     // everything in the given folder
 225     // (IOUtils.copyfiles() have recursive behavior)
 226     List<File> expandFileset(File root) {
 227         List<File> files = new LinkedList<>();
 228         if (!Files.isSymbolicLink(root.toPath())) {
 229             if (root.isDirectory()) {
 230                 File[] children = root.listFiles();
 231                 if (children != null) {
 232                     for (File f : children) {
 233                         files.addAll(expandFileset(f));
 234                     }
 235                 }
 236             } else {
 237                 files.add(root);
 238             }
 239         }
 240         return files;
 241     }
 242 
 243     public void addResource(File baseDir, String path) {
 244         File file = new File(baseDir, path);
 245         // normalize top level dir
 246         // to strip things like "." in the path
 247         // or it can confuse symlink detection logic
 248         file = file.getAbsoluteFile();
 249 
 250         if (baseDir == null) {
 251             baseDir = file.getParentFile();
 252         }
 253         resources.add(new RelativeFileSet(
 254                 baseDir, new LinkedHashSet<>(expandFileset(file))));
 255     }
 256 
 257     public void addResource(File baseDir, File file) {
 258         // normalize initial file
 259         // to strip things like "." in the path
 260         // or it can confuse symlink detection logic
 261         file = file.getAbsoluteFile();
 262 
 263         if (baseDir == null) {
 264             baseDir = file.getParentFile();
 265         }
 266         resources.add(new RelativeFileSet(
 267                 baseDir, new LinkedHashSet<>(expandFileset(file))));
 268     }
 269 
 270     void setClasspath() {
 271         String classpath = "";
 272         for (RelativeFileSet resource : resources) {
 273              for (String file : resource.getIncludedFiles()) {
 274                  if (file.endsWith(".jar")) {
 275                      classpath += file + File.pathSeparator;
 276                  }
 277              }
 278         }
 279         addBundleArgument(
 280                 StandardBundlerParam.CLASSPATH.getID(), classpath);
 281     }
 282 
 283     private static File createFile(final File baseDir, final String path) {
 284         final File testFile = new File(path);
 285         return testFile.isAbsolute() ?
 286                 testFile : new File(baseDir == null ?
 287                         null : baseDir.getAbsolutePath(), path);
 288     }
 289 
 290     static void validateName(String s, boolean forApp)
 291             throws PackagerException {
 292         
 293         String exceptionKey = forApp ?
 294             "ERR_InvalidAppName" : "ERR_InvalidSLName";
 295         
 296         if (s == null) {
 297             if (forApp) {
 298                 return;
 299             } else {
 300                 throw new PackagerException(exceptionKey, s);
 301             }
 302         }
 303         if (s.length() == 0 || s.charAt(s.length() - 1) == '\\') {
 304             throw new PackagerException(exceptionKey, s);
 305         }
 306         try {
 307             // name must be valid path element for this file system
 308             Path p = (new File(s)).toPath();
 309             // and it must be a single name element in a path
 310             if (p.getNameCount() != 1) {
 311                 throw new PackagerException(exceptionKey, s);
 312             }
 313         } catch (InvalidPathException ipe) {
 314             throw new PackagerException(ipe, exceptionKey, s);
 315         }
 316 
 317         for (int i = 0; i < s.length(); i++) {
 318             char a = s.charAt(i);
 319             // We check for ASCII codes first which we accept. If check fails,
 320             // check if it is acceptable extended ASCII or unicode character.
 321             if (a < ' ' || a > '~') {
 322                 // Accept anything else including special chars like copyright
 323                 // symbols. Note: space will be included by ASCII check above,
 324                 // but other whitespace like tabs or new line will be rejected.
 325                 if (Character.isISOControl(a)  ||
 326                         Character.isWhitespace(a)) {
 327                     throw new PackagerException(exceptionKey, s);
 328                 }
 329             } else if (a == '"' || a == '%') {
 330                 throw new PackagerException(exceptionKey, s);
 331             }
 332         }
 333     }
 334 
 335     public void validate() throws PackagerException {
 336         if (outdir == null) {
 337             throw new PackagerException("ERR_MissingArgument", "--output");
 338         }
 339 
 340         boolean hasModule = (bundlerArguments.get(
 341                 Arguments.CLIOptions.MODULE.getId()) != null);
 342         boolean hasImage = (bundlerArguments.get(
 343                 Arguments.CLIOptions.PREDEFINED_APP_IMAGE.getId()) != null);
 344         boolean hasClass = (bundlerArguments.get(
 345                 Arguments.CLIOptions.APPCLASS.getId()) != null);
 346         boolean hasMain = (bundlerArguments.get(
 347                 Arguments.CLIOptions.MAIN_JAR.getId()) != null);
 348         boolean hasRuntimeImage = (bundlerArguments.get(
 349                 Arguments.CLIOptions.PREDEFINED_RUNTIME_IMAGE.getId()) != null);
 350         boolean hasInput = (bundlerArguments.get(
 351                 Arguments.CLIOptions.INPUT.getId()) != null);
 352         boolean hasModulePath = (bundlerArguments.get(
 353                 Arguments.CLIOptions.MODULE_PATH.getId()) != null);
 354         boolean hasAppImage = (bundlerArguments.get(
 355                 Arguments.CLIOptions.PREDEFINED_APP_IMAGE.getId()) != null);
 356         boolean runtimeInstaller = (bundlerArguments.get(
 357                 Arguments.CLIOptions.RUNTIME_INSTALLER.getId()) != null);
 358 
 359         if (getBundleType() == BundlerType.IMAGE) {
 360             // Module application requires --runtime-image or --module-path
 361             if (hasModule) {
 362                 if (!hasModulePath && !hasRuntimeImage) {
 363                     throw new PackagerException("ERR_MissingArgument",
 364                             "--runtime-image or --module-path");
 365                 }
 366             } else {
 367                 if (!hasInput) {
 368                     throw new PackagerException(
 369                            "ERR_MissingArgument", "--input");
 370                 }
 371             }
 372         } else if (getBundleType() == BundlerType.INSTALLER) {
 373             if (!runtimeInstaller) {
 374                 if (hasModule) {
 375                     if (!hasModulePath && !hasRuntimeImage && !hasAppImage) {
 376                         throw new PackagerException("ERR_MissingArgument",
 377                             "--runtime-image, --module-path or --app-image");
 378                     }
 379                 } else {
 380                     if (!hasInput && !hasAppImage) {
 381                         throw new PackagerException("ERR_MissingArgument",
 382                                 "--input or --app-image");
 383                     }
 384                 }
 385             }
 386         }
 387 
 388         // if bundling non-modular image, or installer without app-image
 389         // then we need some resources and a main class
 390         if (!hasModule && !hasImage && !runtimeInstaller) {
 391             if (resources.isEmpty()) {
 392                 throw new PackagerException("ERR_MissingAppResources");
 393             }
 394             if (!hasClass) {
 395                 throw new PackagerException("ERR_MissingArgument",
 396                         "--main-class");
 397             }
 398             if (!hasMain) {
 399                 throw new PackagerException("ERR_MissingArgument",
 400                         "--main-jar");
 401             }
 402         }
 403 
 404         String name = (String)bundlerArguments.get(
 405                 Arguments.CLIOptions.NAME.getId());
 406         validateName(name, true);
 407 
 408         // Validate app image if set
 409         String appImage = (String)bundlerArguments.get(
 410                 Arguments.CLIOptions.PREDEFINED_APP_IMAGE.getId());
 411         if (appImage != null) {
 412             File appImageDir = new File(appImage);
 413             if (!appImageDir.exists() || appImageDir.list().length == 0) {
 414                 throw new PackagerException("ERR_AppImageNotExist", appImage);
 415             }
 416         }
 417 
 418         // Validate build-root
 419         String root = (String)bundlerArguments.get(
 420                 Arguments.CLIOptions.BUILD_ROOT.getId());
 421         if (root != null) {
 422             String [] contents = (new File(root)).list();
 423 
 424             if (contents != null && contents.length > 0) {
 425                 throw new PackagerException("ERR_BuildRootInvalid", root);
 426             }
 427         }
 428 
 429         // Validate license file if set
 430         String license = (String)bundlerArguments.get(
 431                 Arguments.CLIOptions.LICENSE_FILE.getId());
 432         if (license != null) {
 433             File licenseFile = new File(license);
 434             if (!licenseFile.exists()) {
 435                 throw new PackagerException("ERR_LicenseFileNotExit");
 436             }
 437         }
 438     }
 439 
 440     boolean validateForBundle() {
 441         boolean result = false;
 442 
 443         // Success
 444         if (((applicationClass != null && !applicationClass.isEmpty()) ||
 445             (module != null && !module.isEmpty()))) {
 446             result = true;
 447         }
 448 
 449         return result;
 450     }
 451 
 452     BundlerType bundleType = BundlerType.NONE;
 453     String targetFormat = null; //means any
 454 
 455     void setBundleType(BundlerType type) {
 456         bundleType = type;
 457     }
 458 
 459     BundlerType getBundleType() {
 460         return bundleType;
 461     }
 462 
 463     void setTargetFormat(String t) {
 464         targetFormat = t;
 465     }
 466 
 467     String getTargetFormat() {
 468         return targetFormat;
 469     }
 470 
 471     private String getArch() {
 472         String arch = System.getProperty("os.arch").toLowerCase();
 473 
 474         if ("x86".equals(arch) || "i386".equals(arch) || "i486".equals(arch)
 475                 || "i586".equals(arch) || "i686".equals(arch)) {
 476             arch = "x86";
 477         } else if ("x86_64".equals(arch) || "amd64".equals("arch")) {
 478             arch = "x86_64";
 479         }
 480 
 481         return arch;
 482     }
 483 
 484     static final Set<String> multi_args = new TreeSet<>(Arrays.asList(
 485             StandardBundlerParam.JVM_OPTIONS.getID(),
 486             StandardBundlerParam.ARGUMENTS.getID(),
 487             StandardBundlerParam.MODULE_PATH.getID(),
 488             StandardBundlerParam.ADD_MODULES.getID(),
 489             StandardBundlerParam.LIMIT_MODULES.getID(),
 490             StandardBundlerParam.FILE_ASSOCIATIONS.getID()
 491     ));
 492 
 493     @SuppressWarnings("unchecked")
 494     public void addBundleArgument(String key, Object value) {
 495         // special hack for multi-line arguments
 496         if (multi_args.contains(key)) {
 497             Object existingValue = bundlerArguments.get(key);
 498             if (existingValue instanceof String && value instanceof String) {
 499                 bundlerArguments.put(key, existingValue + "\n\n" + value);
 500             } else if (existingValue instanceof List && value instanceof List) {
 501                 ((List)existingValue).addAll((List)value);
 502             } else if (existingValue instanceof Map &&
 503                 value instanceof String && ((String)value).contains("=")) {
 504                 String[] mapValues = ((String)value).split("=", 2);
 505                 ((Map)existingValue).put(mapValues[0], mapValues[1]);
 506             } else {
 507                 bundlerArguments.put(key, value);
 508             }
 509         } else {
 510             bundlerArguments.put(key, value);
 511         }
 512     }
 513 
 514     BundleParams getBundleParams() {
 515         BundleParams bundleParams = new BundleParams();
 516 
 517         // construct app resources relative to output folder!
 518         bundleParams.setAppResourcesList(resources);
 519 
 520         bundleParams.setIdentifier(id);
 521 
 522         bundleParams.setApplicationClass(applicationClass);
 523         bundleParams.setAppVersion(version);
 524         bundleParams.setType(bundleType);
 525         bundleParams.setBundleFormat(targetFormat);
 526         bundleParams.setVendor(vendor);
 527         bundleParams.setEmail(email);
 528         bundleParams.setInstalldirChooser(installdirChooser);
 529         bundleParams.setCopyright(copyright);
 530         bundleParams.setApplicationCategory(category);
 531         bundleParams.setDescription(description);
 532         bundleParams.setTitle(title);
 533 
 534         bundleParams.setJvmargs(jvmargs);
 535         bundleParams.setArguments(arguments);
 536 
 537         if (addModules != null && !addModules.isEmpty()) {
 538             bundleParams.setAddModules(addModules);
 539         }
 540 
 541         if (limitModules != null && !limitModules.isEmpty()) {
 542             bundleParams.setLimitModules(limitModules);
 543         }
 544 
 545         if (stripNativeCommands != null) {
 546             bundleParams.setStripNativeCommands(stripNativeCommands);
 547         }
 548 
 549         if (modulePath != null && !modulePath.isEmpty()) {
 550             bundleParams.setModulePath(modulePath);
 551         }
 552 
 553         if (module != null && !module.isEmpty()) {
 554             bundleParams.setMainModule(module);
 555         }
 556 
 557         if (debugPort != null && !debugPort.isEmpty()) {
 558             bundleParams.setDebug(debugPort);
 559         }
 560 
 561         Map<String, String> paramsMap = new TreeMap<>();
 562         if (params != null) {
 563             for (Param p : params) {
 564                 paramsMap.put(p.name, p.value);
 565             }
 566         }
 567 
 568         Map<String, String> unescapedHtmlParams = new TreeMap<>();
 569         Map<String, String> escapedHtmlParams = new TreeMap<>();
 570 
 571         // check for collisions
 572         TreeSet<String> keys = new TreeSet<>(bundlerArguments.keySet());
 573         keys.retainAll(bundleParams.getBundleParamsAsMap().keySet());
 574 
 575         if (!keys.isEmpty()) {
 576             throw new RuntimeException("Deploy Params and Bundler Arguments "
 577                     + "overlap in the following values:" + keys.toString());
 578         }
 579 
 580         bundleParams.addAllBundleParams(bundlerArguments);
 581 
 582         return bundleParams;
 583     }
 584 
 585     Map<String, ? super Object> getBundlerArguments() {
 586         return this.bundlerArguments;
 587     }
 588 
 589     void putUnlessNull(String param, Object value) {
 590         if (value != null) {
 591             bundlerArguments.put(param, value);
 592         }
 593     }
 594 
 595     void putUnlessNullOrEmpty(String param, Map<?, ?> value) {
 596         if (value != null && !value.isEmpty()) {
 597             bundlerArguments.put(param, value);
 598         }
 599     }
 600 
 601     void putUnlessNullOrEmpty(String param, Collection<?> value) {
 602         if (value != null && !value.isEmpty()) {
 603             bundlerArguments.put(param, value);
 604         }
 605     }
 606 
 607     @Override
 608     public String toString() {
 609         return "DeployParams {" + "output: " + outdir
 610                 + " resources: {" + resources + "}}";
 611     }
 612 
 613 }