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.text.MessageFormat;
  31 import java.util.ArrayList;
  32 import java.util.Arrays;
  33 import java.util.Collection;
  34 import java.util.LinkedHashMap;
  35 import java.util.LinkedHashSet;
  36 import java.util.LinkedList;
  37 import java.util.List;
  38 import java.util.Map;
  39 import java.util.Set;
  40 import java.util.TreeMap;
  41 import java.util.TreeSet;
  42 
  43 /**
  44  * DeployParams
  45  *
  46  * This class is generated and used in Arguments.processArguments() as
  47  * intermediate step in generating the BundleParams and ultimately the Bundles
  48  */
  49 public class DeployParams {
  50 
  51     final List<RelativeFileSet> resources = new ArrayList<>();
  52 
  53     String id;
  54     String title;
  55     String vendor;
  56     String email;
  57     String description;
  58     String category;
  59     String licenseType;
  60     String copyright;
  61     String version;
  62     Boolean systemWide;
  63     Boolean serviceHint;
  64     Boolean signBundle;
  65     Boolean installdirChooser;
  66 
  67     String applicationClass;
  68 
  69     List<Param> params;
  70     List<String> arguments; //unnamed arguments
  71 
  72     // Java 9 modules support
  73     String addModules = null;
  74     String limitModules = null;
  75     Boolean stripNativeCommands = null;
  76     String modulePath = null;
  77     String module = null;
  78     String debugPort = null;
  79 
  80     boolean jreInstaller = false;
  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     void setJreInstaller(boolean value) {
 204         jreInstaller = value;
 205     }
 206 
 207     File getOutput() {
 208         return outdir;
 209     }
 210 
 211     public void setOutput(File output) {
 212         outdir = output;
 213     }
 214 
 215     static class Template {
 216         File in;
 217         File out;
 218 
 219         Template(File in, File out) {
 220             this.in = in;
 221             this.out = out;
 222         }
 223     }
 224 
 225     // we need to expand as in some cases
 226     // (most notably jpackage)
 227     // we may get "." as filename and assumption is we include
 228     // everything in the given folder
 229     // (IOUtils.copyfiles() have recursive behavior)
 230     List<File> expandFileset(File root) {
 231         List<File> files = new LinkedList<>();
 232         if (!Files.isSymbolicLink(root.toPath())) {
 233             if (root.isDirectory()) {
 234                 File[] children = root.listFiles();
 235                 if (children != null) {
 236                     for (File f : children) {
 237                         files.addAll(expandFileset(f));
 238                     }
 239                 }
 240             } else {
 241                 files.add(root);
 242             }
 243         }
 244         return files;
 245     }
 246 
 247     public void addResource(File baseDir, String path) {
 248         File file = new File(baseDir, path);
 249         // normalize top level dir
 250         // to strip things like "." in the path
 251         // or it can confuse symlink detection logic
 252         file = file.getAbsoluteFile();
 253 
 254         if (baseDir == null) {
 255             baseDir = file.getParentFile();
 256         }
 257         resources.add(new RelativeFileSet(
 258                 baseDir, new LinkedHashSet<>(expandFileset(file))));
 259     }
 260 
 261     public void addResource(File baseDir, File file) {
 262         // normalize initial file
 263         // to strip things like "." in the path
 264         // or it can confuse symlink detection logic
 265         file = file.getAbsoluteFile();
 266 
 267         if (baseDir == null) {
 268             baseDir = file.getParentFile();
 269         }
 270         resources.add(new RelativeFileSet(
 271                 baseDir, new LinkedHashSet<>(expandFileset(file))));
 272     }
 273 
 274     void setClasspath() {
 275         String classpath = "";
 276         for (RelativeFileSet resource : resources) {
 277              for (String file : resource.getIncludedFiles()) {
 278                  if (file.endsWith(".jar")) {
 279                      classpath += file + File.pathSeparator;
 280                  }
 281              }
 282         }
 283         addBundleArgument(
 284                 StandardBundlerParam.CLASSPATH.getID(), classpath);
 285     }
 286 
 287     private static File createFile(final File baseDir, final String path) {
 288         final File testFile = new File(path);
 289         return testFile.isAbsolute() ?
 290                 testFile : new File(baseDir == null ?
 291                         null : baseDir.getAbsolutePath(), path);
 292     }
 293 
 294     static void validateAppName(String s) throws PackagerException {
 295         if (s == null || s.length() == 0) {
 296             // empty or null string - there is no unsupported char
 297             return;
 298         }
 299 
 300         int last = s.length() - 1;
 301 
 302         char fc = s.charAt(0);
 303         char lc = s.charAt(last);
 304 
 305         // illegal to end in backslash escape char
 306         if (lc == '\\') {
 307             throw new PackagerException("ERR_InvalidCharacterInArgument", "--name");
 308         }
 309 
 310         for (int i = 0; i < s.length(); i++) {
 311             char a = s.charAt(i);
 312             // We check for ASCII codes first which we accept. If check fails,
 313             // check if it is acceptable extended ASCII or unicode character.
 314             if (a < ' ' || a > '~' || a == '%') {
 315                 // Reject '%', whitespaces and ISO Control.
 316                 // Accept anything else including special chars like copyright
 317                 // symbols. Note: space will be included by ASCII check above,
 318                 // but other whitespace like tabs or new line will be ignored.
 319                 if (Character.isISOControl(a) ||
 320                         Character.isWhitespace(a) || a == '%') {
 321                     throw new PackagerException(
 322                             "ERR_InvalidCharacterInArgument", "--name");
 323                 }
 324             }
 325             if (a == '"') {
 326                 throw new PackagerException(
 327                         "ERR_InvalidCharacterInArgument", "--name");
 328             }
 329         }
 330     }
 331 
 332     public void validate() throws PackagerException {
 333         if (outdir == null) {
 334             throw new PackagerException("ERR_MissingArgument", "--output");
 335         }
 336 
 337         boolean hasModule = (bundlerArguments.get(
 338                 Arguments.CLIOptions.MODULE.getId()) != null);
 339         boolean hasImage = (bundlerArguments.get(
 340                 Arguments.CLIOptions.PREDEFINED_APP_IMAGE.getId()) != null);
 341         boolean hasClass = (bundlerArguments.get(
 342                 Arguments.CLIOptions.APPCLASS.getId()) != null);
 343         boolean hasMain = (bundlerArguments.get(
 344                 Arguments.CLIOptions.MAIN_JAR.getId()) != null);
 345         boolean hasRuntimeImage = (bundlerArguments.get(
 346                 Arguments.CLIOptions.PREDEFINED_RUNTIME_IMAGE.getId()) != null);
 347         boolean hasInput = (bundlerArguments.get(
 348                 Arguments.CLIOptions.INPUT.getId()) != null);
 349         boolean hasModulePath = (bundlerArguments.get(
 350                 Arguments.CLIOptions.MODULE_PATH.getId()) != null);
 351         boolean hasAppImage = (bundlerArguments.get(
 352                 Arguments.CLIOptions.PREDEFINED_APP_IMAGE.getId()) != null);
 353 
 354         if (getBundleType() == BundlerType.IMAGE) {
 355             // Module application requires --runtime-image or --module-path
 356             if (hasModule) {
 357                 if (!hasModulePath && !hasRuntimeImage) {
 358                     throw new PackagerException("ERR_MissingArgument",
 359                             "--runtime-image or --module-path");
 360                 }
 361             } else {
 362                 if (!hasInput) {
 363                     throw new PackagerException(
 364                            "ERR_MissingArgument", "--input");
 365                 }
 366             }
 367         } else if (getBundleType() == BundlerType.INSTALLER) {
 368             if (!Arguments.isJreInstaller()) {
 369                 if (hasModule) {
 370                     if (!hasModulePath && !hasRuntimeImage && !hasAppImage) {
 371                         throw new PackagerException("ERR_MissingArgument",
 372                             "--runtime-image, --module-path or --app-image");
 373                     }
 374                 } else {
 375                     if (!hasInput && !hasAppImage) {
 376                         throw new PackagerException("ERR_MissingArgument",
 377                                 "--input or --app-image");
 378                     }
 379                 }
 380             }
 381         }
 382 
 383         // if bundling non-modular image, or installer without app-image
 384         // then we need some resources and a main class
 385         if (!hasModule && !hasImage && !jreInstaller) {
 386             if (resources.isEmpty()) {
 387                 throw new PackagerException("ERR_MissingAppResources");
 388             }
 389             if (!hasClass) {
 390                 throw new PackagerException("ERR_MissingArgument", "--class");
 391             }
 392             if (!hasMain) {
 393                 throw new PackagerException("ERR_MissingArgument",
 394                         "--main-jar");
 395             }
 396         }
 397 
 398         String name = (String)bundlerArguments.get(
 399                 Arguments.CLIOptions.NAME.getId());
 400         validateAppName(name);
 401 
 402         // Validate app image if set
 403         String appImage = (String)bundlerArguments.get(
 404                 Arguments.CLIOptions.PREDEFINED_APP_IMAGE.getId());
 405         if (appImage != null) {
 406             File appImageDir = new File(appImage);
 407             if (!appImageDir.exists()) {
 408                 throw new PackagerException("ERR_AppImageNotExist", appImage);
 409             }
 410 
 411             File appImageAppDir = new File(appImage + File.separator + "app");
 412             File appImageRuntimeDir = new File(appImage
 413                     + File.separator + "runtime");
 414             if (!appImageAppDir.exists() || !appImageRuntimeDir.exists()) {
 415                 throw new PackagerException("ERR_AppImageInvalid", appImage);
 416             }
 417         }
 418 
 419         // Validate build-root
 420         String root = (String)bundlerArguments.get(
 421                 Arguments.CLIOptions.BUILD_ROOT.getId());
 422         if (root != null) {
 423             String [] contents = (new File(root)).list();
 424 
 425             if (contents != null && contents.length > 0) {
 426                 throw new PackagerException("ERR_BuildRootInvalid", root);
 427             }
 428         }
 429 
 430         // Validate license file if set
 431         String license = (String)bundlerArguments.get(
 432                 Arguments.CLIOptions.LICENSE_FILE.getId());
 433         if (license != null) {
 434             File licenseFile = new File(license);
 435             if (!licenseFile.exists()) {
 436                 throw new PackagerException("ERR_LicenseFileNotExit");
 437             }
 438         }
 439     }
 440 
 441     boolean validateForBundle() {
 442         boolean result = false;
 443 
 444         // Success
 445         if (((applicationClass != null && !applicationClass.isEmpty()) ||
 446             (module != null && !module.isEmpty()))) {
 447             result = true;
 448         }
 449 
 450         return result;
 451     }
 452 
 453     BundlerType bundleType = BundlerType.NONE;
 454     String targetFormat = null; //means any
 455 
 456     void setBundleType(BundlerType type) {
 457         bundleType = type;
 458     }
 459 
 460     BundlerType getBundleType() {
 461         return bundleType;
 462     }
 463 
 464     void setTargetFormat(String t) {
 465         targetFormat = t;
 466     }
 467 
 468     String getTargetFormat() {
 469         return targetFormat;
 470     }
 471 
 472     private String getArch() {
 473         String arch = System.getProperty("os.arch").toLowerCase();
 474 
 475         if ("x86".equals(arch) || "i386".equals(arch) || "i486".equals(arch)
 476                 || "i586".equals(arch) || "i686".equals(arch)) {
 477             arch = "x86";
 478         } else if ("x86_64".equals(arch) || "amd64".equals("arch")) {
 479             arch = "x86_64";
 480         }
 481 
 482         return arch;
 483     }
 484 
 485     static final Set<String> multi_args = new TreeSet<>(Arrays.asList(
 486             StandardBundlerParam.JVM_OPTIONS.getID(),
 487             StandardBundlerParam.ARGUMENTS.getID(),
 488             StandardBundlerParam.MODULE_PATH.getID(),
 489             StandardBundlerParam.ADD_MODULES.getID(),
 490             StandardBundlerParam.LIMIT_MODULES.getID(),
 491             StandardBundlerParam.FILE_ASSOCIATIONS.getID()
 492     ));
 493 
 494     @SuppressWarnings("unchecked")
 495     public void addBundleArgument(String key, Object value) {
 496         // special hack for multi-line arguments
 497         if (multi_args.contains(key)) {
 498             Object existingValue = bundlerArguments.get(key);
 499             if (existingValue instanceof String && value instanceof String) {
 500                 bundlerArguments.put(key, existingValue + "\n\n" + value);
 501             } else if (existingValue instanceof List && value instanceof List) {
 502                 ((List)existingValue).addAll((List)value);
 503             } else if (existingValue instanceof Map &&
 504                 value instanceof String && ((String)value).contains("=")) {
 505                 String[] mapValues = ((String)value).split("=", 2);
 506                 ((Map)existingValue).put(mapValues[0], mapValues[1]);
 507             } else {
 508                 bundlerArguments.put(key, value);
 509             }
 510         } else {
 511             bundlerArguments.put(key, value);
 512         }
 513     }
 514 
 515     BundleParams getBundleParams() {
 516         BundleParams bundleParams = new BundleParams();
 517 
 518         // construct app resources relative to output folder!
 519         String currentOS = System.getProperty("os.name").toLowerCase();
 520         String currentArch = getArch();
 521 
 522         bundleParams.setAppResourcesList(resources);
 523 
 524         bundleParams.setIdentifier(id);
 525 
 526         bundleParams.setApplicationClass(applicationClass);
 527         bundleParams.setAppVersion(version);
 528         bundleParams.setType(bundleType);
 529         bundleParams.setBundleFormat(targetFormat);
 530         bundleParams.setVendor(vendor);
 531         bundleParams.setEmail(email);
 532         bundleParams.setInstalldirChooser(installdirChooser);
 533         bundleParams.setCopyright(copyright);
 534         bundleParams.setApplicationCategory(category);
 535         bundleParams.setDescription(description);
 536         bundleParams.setTitle(title);
 537 
 538         bundleParams.setJvmargs(jvmargs);
 539         bundleParams.setArguments(arguments);
 540 
 541         if (addModules != null && !addModules.isEmpty()) {
 542             bundleParams.setAddModules(addModules);
 543         }
 544 
 545         if (limitModules != null && !limitModules.isEmpty()) {
 546             bundleParams.setLimitModules(limitModules);
 547         }
 548 
 549         if (stripNativeCommands != null) {
 550             bundleParams.setStripNativeCommands(stripNativeCommands);
 551         }
 552 
 553         if (modulePath != null && !modulePath.isEmpty()) {
 554             bundleParams.setModulePath(modulePath);
 555         }
 556 
 557         if (module != null && !module.isEmpty()) {
 558             bundleParams.setMainModule(module);
 559         }
 560 
 561         if (debugPort != null && !debugPort.isEmpty()) {
 562             bundleParams.setDebug(debugPort);
 563         }
 564 
 565         Map<String, String> paramsMap = new TreeMap<>();
 566         if (params != null) {
 567             for (Param p : params) {
 568                 paramsMap.put(p.name, p.value);
 569             }
 570         }
 571 
 572         Map<String, String> unescapedHtmlParams = new TreeMap<>();
 573         Map<String, String> escapedHtmlParams = new TreeMap<>();
 574 
 575         // check for collisions
 576         TreeSet<String> keys = new TreeSet<>(bundlerArguments.keySet());
 577         keys.retainAll(bundleParams.getBundleParamsAsMap().keySet());
 578 
 579         if (!keys.isEmpty()) {
 580             throw new RuntimeException("Deploy Params and Bundler Arguments "
 581                     + "overlap in the following values:" + keys.toString());
 582         }
 583 
 584         bundleParams.addAllBundleParams(bundlerArguments);
 585 
 586         return bundleParams;
 587     }
 588 
 589     Map<String, ? super Object> getBundlerArguments() {
 590         return this.bundlerArguments;
 591     }
 592 
 593     void putUnlessNull(String param, Object value) {
 594         if (value != null) {
 595             bundlerArguments.put(param, value);
 596         }
 597     }
 598 
 599     void putUnlessNullOrEmpty(String param, Map<?, ?> value) {
 600         if (value != null && !value.isEmpty()) {
 601             bundlerArguments.put(param, value);
 602         }
 603     }
 604 
 605     void putUnlessNullOrEmpty(String param, Collection<?> value) {
 606         if (value != null && !value.isEmpty()) {
 607             bundlerArguments.put(param, value);
 608         }
 609     }
 610 
 611     @Override
 612     public String toString() {
 613         return "DeployParams {" + "output: " + outdir
 614                 + " resources: {" + resources + "}}";
 615     }
 616 
 617 }