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