1 /* 2 * Copyright (c) 2018, 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 package jdk.jpackage.internal; 26 27 import java.io.File; 28 import java.io.FileInputStream; 29 import java.io.IOException; 30 import java.nio.file.Files; 31 import java.nio.file.Path; 32 import java.text.MessageFormat; 33 import java.util.ArrayList; 34 import java.util.Arrays; 35 import java.util.Collection; 36 import java.util.EnumSet; 37 import java.util.HashMap; 38 import java.util.HashSet; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.Set; 42 import java.util.Properties; 43 import java.util.ResourceBundle; 44 import java.util.jar.Attributes; 45 import java.util.jar.JarFile; 46 import java.util.jar.Manifest; 47 import java.util.stream.Stream; 48 import java.util.regex.Matcher; 49 import java.util.regex.Pattern; 50 51 /** 52 * Arguments 53 * 54 * This class encapsulates and processes the command line arguments, 55 * in effect, implementing all the work of jpackage tool. 56 * 57 * The primary entry point, processArguments(): 58 * Processes and validates command line arguments, constructing DeployParams. 59 * Validates the DeployParams, and generate the BundleParams. 60 * Generates List of Bundlers from BundleParams valid for this platform. 61 * Executes each Bundler in the list. 62 */ 63 public class Arguments { 64 private static final ResourceBundle I18N = ResourceBundle.getBundle( 65 "jdk.jpackage.internal.resources.MainResources"); 66 67 private static final String FA_EXTENSIONS = "extension"; 68 private static final String FA_CONTENT_TYPE = "mime-type"; 69 private static final String FA_DESCRIPTION = "description"; 70 private static final String FA_ICON = "icon"; 71 72 // regexp for parsing args (for example, for additional launchers) 73 private static Pattern pattern = Pattern.compile( 74 "(?:(?:([\"'])(?:\\\\\\1|.)*?(?:\\1|$))|(?:\\\\[\"'\\s]|[^\\s]))++"); 75 76 private DeployParams deployParams = null; 77 78 private int pos = 0; 79 private List<String> argList = null; 80 81 private List<CLIOptions> allOptions = null; 82 83 private String input = null; 84 private String output = null; 85 86 private boolean hasMainJar = false; 87 private boolean hasMainClass = false; 88 private boolean hasMainModule = false; 89 public boolean userProvidedBuildRoot = false; 90 91 private String buildRoot = null; 92 private String mainJarPath = null; 93 94 private static boolean runtimeInstaller = false; 95 96 private List<AddLauncherArguments> addLaunchers = null; 97 98 private static Map<String, CLIOptions> argIds = new HashMap<>(); 99 private static Map<String, CLIOptions> argShortIds = new HashMap<>(); 100 101 static { 102 // init maps for parsing arguments 103 (EnumSet.allOf(CLIOptions.class)).forEach(option -> { 104 argIds.put(option.getIdWithPrefix(), option); 105 if (option.getShortIdWithPrefix() != null) { 106 argShortIds.put(option.getShortIdWithPrefix(), option); 107 } 108 }); 109 } 110 111 public Arguments(String[] args) { 112 argList = new ArrayList<String>(args.length); 113 for (String arg : args) { 114 argList.add(arg); 115 } 116 Log.verbose ("\njpackage argument list: \n" + argList + "\n"); 117 pos = 0; 118 119 deployParams = new DeployParams(); 120 121 allOptions = new ArrayList<>(); 122 123 addLaunchers = new ArrayList<>(); 124 } 125 126 // CLIOptions is public for DeployParamsTest 127 public enum CLIOptions { 128 PACKAGE_TYPE("package-type", OptionCategories.PROPERTY, () -> { 129 context().deployParams.setTargetFormat(popArg()); 130 }), 131 132 INPUT ("input", "i", OptionCategories.PROPERTY, () -> { 133 context().input = popArg(); 134 setOptionValue("input", context().input); 135 }), 136 137 OUTPUT ("output", "o", OptionCategories.PROPERTY, () -> { 138 context().output = popArg(); 139 context().deployParams.setOutput(new File(context().output)); 140 }), 141 142 DESCRIPTION ("description", "d", OptionCategories.PROPERTY), 143 144 VENDOR ("vendor", OptionCategories.PROPERTY), 145 146 APPCLASS ("main-class", OptionCategories.PROPERTY, () -> { 147 context().hasMainClass = true; 148 setOptionValue("main-class", popArg()); 149 }), 150 151 NAME ("name", "n", OptionCategories.PROPERTY), 152 153 IDENTIFIER ("identifier", OptionCategories.PROPERTY), 154 155 VERBOSE ("verbose", OptionCategories.PROPERTY, () -> { 156 setOptionValue("verbose", true); 157 Log.setVerbose(); 158 }), 159 160 RESOURCE_DIR("resource-dir", 161 OptionCategories.PROPERTY, () -> { 162 String resourceDir = popArg(); 163 setOptionValue("resource-dir", resourceDir); 164 }), 165 166 ARGUMENTS ("arguments", OptionCategories.PROPERTY, () -> { 167 List<String> arguments = getArgumentList(popArg()); 168 setOptionValue("arguments", arguments); 169 }), 170 171 ICON ("icon", OptionCategories.PROPERTY), 172 173 COPYRIGHT ("copyright", OptionCategories.PROPERTY), 174 175 LICENSE_FILE ("license-file", OptionCategories.PROPERTY), 176 177 VERSION ("app-version", OptionCategories.PROPERTY), 178 179 RELEASE ("linux-app-release", OptionCategories.PROPERTY), 180 181 JAVA_OPTIONS ("java-options", OptionCategories.PROPERTY, () -> { 182 List<String> args = getArgumentList(popArg()); 183 args.forEach(a -> setOptionValue("java-options", a)); 184 }), 185 186 FILE_ASSOCIATIONS ("file-associations", 187 OptionCategories.PROPERTY, () -> { 188 Map<String, ? super Object> args = new HashMap<>(); 189 190 // load .properties file 191 Map<String, String> initialMap = getPropertiesFromFile(popArg()); 192 193 String ext = initialMap.get(FA_EXTENSIONS); 194 if (ext != null) { 195 args.put(StandardBundlerParam.FA_EXTENSIONS.getID(), ext); 196 } 197 198 String type = initialMap.get(FA_CONTENT_TYPE); 199 if (type != null) { 200 args.put(StandardBundlerParam.FA_CONTENT_TYPE.getID(), type); 201 } 202 203 String desc = initialMap.get(FA_DESCRIPTION); 204 if (desc != null) { 205 args.put(StandardBundlerParam.FA_DESCRIPTION.getID(), desc); 206 } 207 208 String icon = initialMap.get(FA_ICON); 209 if (icon != null) { 210 args.put(StandardBundlerParam.FA_ICON.getID(), icon); 211 } 212 213 ArrayList<Map<String, ? super Object>> associationList = 214 new ArrayList<Map<String, ? super Object>>(); 215 216 associationList.add(args); 217 218 // check that we really add _another_ value to the list 219 setOptionValue("file-associations", associationList); 220 221 }), 222 223 ADD_LAUNCHER ("add-launcher", 224 OptionCategories.PROPERTY, () -> { 225 String spec = popArg(); 226 String name = null; 227 String filename = spec; 228 if (spec.contains("=")) { 229 String[] values = spec.split("=", 2); 230 name = values[0]; 231 filename = values[1]; 232 } 233 context().addLaunchers.add( 234 new AddLauncherArguments(name, filename)); 235 }), 236 237 TEMP_ROOT ("temp-root", OptionCategories.PROPERTY, () -> { 238 context().buildRoot = popArg(); 239 context().userProvidedBuildRoot = true; 240 setOptionValue("temp-root", context().buildRoot); 241 }), 242 243 INSTALL_DIR ("install-dir", OptionCategories.PROPERTY), 244 245 PREDEFINED_APP_IMAGE ("app-image", OptionCategories.PROPERTY), 246 247 PREDEFINED_RUNTIME_IMAGE ("runtime-image", OptionCategories.PROPERTY), 248 249 MAIN_JAR ("main-jar", OptionCategories.PROPERTY, () -> { 250 context().mainJarPath = popArg(); 251 context().hasMainJar = true; 252 setOptionValue("main-jar", context().mainJarPath); 253 }), 254 255 MODULE ("module", "m", OptionCategories.MODULAR, () -> { 256 context().hasMainModule = true; 257 setOptionValue("module", popArg()); 258 }), 259 260 ADD_MODULES ("add-modules", OptionCategories.MODULAR), 261 262 MODULE_PATH ("module-path", "p", OptionCategories.MODULAR), 263 264 MAC_SIGN ("mac-sign", "s", OptionCategories.PLATFORM_MAC, () -> { 265 setOptionValue("mac-sign", true); 266 }), 267 268 MAC_BUNDLE_NAME ("mac-bundle-name", OptionCategories.PLATFORM_MAC), 269 270 MAC_BUNDLE_IDENTIFIER("mac-bundle-identifier", 271 OptionCategories.PLATFORM_MAC), 272 273 MAC_APP_STORE_CATEGORY ("mac-app-store-category", 274 OptionCategories.PLATFORM_MAC), 275 276 MAC_BUNDLE_SIGNING_PREFIX ("mac-bundle-signing-prefix", 277 OptionCategories.PLATFORM_MAC), 278 279 MAC_SIGNING_KEY_NAME ("mac-signing-key-user-name", 280 OptionCategories.PLATFORM_MAC), 281 282 MAC_SIGNING_KEYCHAIN ("mac-signing-keychain", 283 OptionCategories.PLATFORM_MAC), 284 285 MAC_APP_STORE_ENTITLEMENTS ("mac-app-store-entitlements", 286 OptionCategories.PLATFORM_MAC), 287 288 WIN_MENU_HINT ("win-menu", OptionCategories.PLATFORM_WIN, () -> { 289 setOptionValue("win-menu", true); 290 }), 291 292 WIN_MENU_GROUP ("win-menu-group", OptionCategories.PLATFORM_WIN), 293 294 WIN_SHORTCUT_HINT ("win-shortcut", 295 OptionCategories.PLATFORM_WIN, () -> { 296 setOptionValue("win-shortcut", true); 297 }), 298 299 WIN_PER_USER_INSTALLATION ("win-per-user-install", 300 OptionCategories.PLATFORM_WIN, () -> { 301 setOptionValue("win-per-user-install", false); 302 }), 303 304 WIN_DIR_CHOOSER ("win-dir-chooser", 305 OptionCategories.PLATFORM_WIN, () -> { 306 setOptionValue("win-dir-chooser", true); 307 }), 308 309 WIN_UPGRADE_UUID ("win-upgrade-uuid", 310 OptionCategories.PLATFORM_WIN), 311 312 WIN_CONSOLE_HINT ("win-console", OptionCategories.PLATFORM_WIN, () -> { 313 setOptionValue("win-console", true); 314 }), 315 316 LINUX_BUNDLE_NAME ("linux-bundle-name", 317 OptionCategories.PLATFORM_LINUX), 318 319 LINUX_DEB_MAINTAINER ("linux-deb-maintainer", 320 OptionCategories.PLATFORM_LINUX), 321 322 LINUX_DEB_COPYRIGHT_FILE ("linux-deb-copyright-file", 323 OptionCategories.PLATFORM_LINUX), 324 325 LINUX_CATEGORY ("linux-app-category", 326 OptionCategories.PLATFORM_LINUX), 327 328 LINUX_RPM_LICENSE_TYPE ("linux-rpm-license-type", 329 OptionCategories.PLATFORM_LINUX), 330 331 LINUX_PACKAGE_DEPENDENCIES ("linux-package-deps", 332 OptionCategories.PLATFORM_LINUX), 333 334 LINUX_MENU_GROUP ("linux-menu-group", OptionCategories.PLATFORM_LINUX); 335 336 private final String id; 337 private final String shortId; 338 private final OptionCategories category; 339 private final ArgAction action; 340 private static Arguments argContext; 341 342 private CLIOptions(String id, OptionCategories category) { 343 this(id, null, category, null); 344 } 345 346 private CLIOptions(String id, String shortId, 347 OptionCategories category) { 348 this(id, shortId, category, null); 349 } 350 351 private CLIOptions(String id, 352 OptionCategories category, ArgAction action) { 353 this(id, null, category, action); 354 } 355 356 private CLIOptions(String id, String shortId, 357 OptionCategories category, ArgAction action) { 358 this.id = id; 359 this.shortId = shortId; 360 this.action = action; 361 this.category = category; 362 } 363 364 static void setContext(Arguments context) { 365 argContext = context; 366 } 367 368 public static Arguments context() { 369 if (argContext != null) { 370 return argContext; 371 } else { 372 throw new RuntimeException("Argument context is not set."); 373 } 374 } 375 376 public String getId() { 377 return this.id; 378 } 379 380 String getIdWithPrefix() { 381 return "--" + this.id; 382 } 383 384 String getShortIdWithPrefix() { 385 return this.shortId == null ? null : "-" + this.shortId; 386 } 387 388 void execute() { 389 if (action != null) { 390 action.execute(); 391 } else { 392 defaultAction(); 393 } 394 } 395 396 private void defaultAction() { 397 context().deployParams.addBundleArgument(id, popArg()); 398 } 399 400 private static void setOptionValue(String option, Object value) { 401 context().deployParams.addBundleArgument(option, value); 402 } 403 404 private static String popArg() { 405 nextArg(); 406 return (context().pos >= context().argList.size()) ? 407 "" : context().argList.get(context().pos); 408 } 409 410 private static String getArg() { 411 return (context().pos >= context().argList.size()) ? 412 "" : context().argList.get(context().pos); 413 } 414 415 private static void nextArg() { 416 context().pos++; 417 } 418 419 private static boolean hasNextArg() { 420 return context().pos < context().argList.size(); 421 } 422 } 423 424 enum OptionCategories { 425 MODULAR, 426 PROPERTY, 427 PLATFORM_MAC, 428 PLATFORM_WIN, 429 PLATFORM_LINUX; 430 } 431 432 public boolean processArguments() { 433 try { 434 435 // init context of arguments 436 CLIOptions.setContext(this); 437 438 // parse cmd line 439 String arg; 440 CLIOptions option; 441 for (; CLIOptions.hasNextArg(); CLIOptions.nextArg()) { 442 arg = CLIOptions.getArg(); 443 if ((option = toCLIOption(arg)) != null) { 444 // found a CLI option 445 allOptions.add(option); 446 option.execute(); 447 } else { 448 throw new PackagerException("ERR_InvalidOption", arg); 449 } 450 } 451 452 if (hasMainJar && !hasMainClass) { 453 // try to get main-class from manifest 454 String mainClass = getMainClassFromManifest(); 455 if (mainClass != null) { 456 CLIOptions.setOptionValue( 457 CLIOptions.APPCLASS.getId(), mainClass); 458 } 459 } 460 461 // display error for arguments that are not supported 462 // for current configuration. 463 464 validateArguments(); 465 466 addResources(deployParams, input, mainJarPath); 467 468 List<Map<String, ? super Object>> launchersAsMap = 469 new ArrayList<>(); 470 471 for (AddLauncherArguments sl : addLaunchers) { 472 launchersAsMap.add(sl.getLauncherMap()); 473 } 474 475 deployParams.addBundleArgument( 476 StandardBundlerParam.ADD_LAUNCHERS.getID(), 477 launchersAsMap); 478 479 // at this point deployParams should be already configured 480 481 deployParams.validate(); 482 483 BundleParams bp = deployParams.getBundleParams(); 484 485 // validate name(s) 486 ArrayList<String> usedNames = new ArrayList<String>(); 487 usedNames.add(bp.getName()); // add main app name 488 489 for (AddLauncherArguments sl : addLaunchers) { 490 Map<String, ? super Object> slMap = sl.getLauncherMap(); 491 String slName = 492 (String) slMap.get(Arguments.CLIOptions.NAME.getId()); 493 if (slName == null) { 494 throw new PackagerException("ERR_NoAddLauncherName"); 495 } 496 // same rules apply to additional launcher names as app name 497 DeployParams.validateName(slName, false); 498 for (String usedName : usedNames) { 499 if (slName.equals(usedName)) { 500 throw new PackagerException("ERR_NoUniqueName"); 501 } 502 } 503 usedNames.add(slName); 504 } 505 if (runtimeInstaller && bp.getName() == null) { 506 throw new PackagerException("ERR_NoJreInstallerName"); 507 } 508 509 generateBundle(bp.getBundleParamsAsMap()); 510 return true; 511 } catch (Exception e) { 512 if (Log.isVerbose()) { 513 Log.verbose(e); 514 } else { 515 String msg1 = e.getMessage(); 516 Log.error(msg1); 517 if (e.getCause() != null && e.getCause() != e) { 518 String msg2 = e.getCause().getMessage(); 519 if (!msg1.contains(msg2)) { 520 Log.error(msg2); 521 } 522 } 523 } 524 return false; 525 } 526 } 527 528 private void validateArguments() throws PackagerException { 529 String type = deployParams.getTargetFormat(); 530 String ptype = (type != null) ? type : "default"; 531 boolean imageOnly = deployParams.isTargetAppImage(); 532 boolean hasAppImage = allOptions.contains( 533 CLIOptions.PREDEFINED_APP_IMAGE); 534 boolean hasRuntime = allOptions.contains( 535 CLIOptions.PREDEFINED_RUNTIME_IMAGE); 536 boolean installerOnly = !imageOnly && hasAppImage; 537 runtimeInstaller = !imageOnly && hasRuntime && !hasAppImage && 538 !hasMainModule && !hasMainJar; 539 540 for (CLIOptions option : allOptions) { 541 if (!ValidOptions.checkIfSupported(option)) { 542 // includes option valid only on different platform 543 throw new PackagerException("ERR_UnsupportedOption", 544 option.getIdWithPrefix()); 545 } 546 if (imageOnly) { 547 if (!ValidOptions.checkIfImageSupported(option)) { 548 throw new PackagerException("ERR_InvalidTypeOption", 549 option.getIdWithPrefix(), type); 550 } 551 } else if (installerOnly || runtimeInstaller) { 552 if (!ValidOptions.checkIfInstallerSupported(option)) { 553 if (runtimeInstaller) { 554 throw new PackagerException("ERR_NoInstallerEntryPoint", 555 option.getIdWithPrefix()); 556 } else { 557 throw new PackagerException("ERR_InvalidTypeOption", 558 option.getIdWithPrefix(), ptype); 559 } 560 } 561 } 562 } 563 if (installerOnly && hasRuntime) { 564 // note --runtime-image is only for image or runtime installer. 565 throw new PackagerException("ERR_InvalidTypeOption", 566 CLIOptions.PREDEFINED_RUNTIME_IMAGE.getIdWithPrefix(), 567 ptype); 568 } 569 if (hasMainJar && hasMainModule) { 570 throw new PackagerException("ERR_BothMainJarAndModule"); 571 } 572 if (imageOnly && !hasMainJar && !hasMainModule) { 573 throw new PackagerException("ERR_NoEntryPoint"); 574 } 575 } 576 577 private jdk.jpackage.internal.Bundler getPlatformBundler() { 578 boolean appImage = deployParams.isTargetAppImage(); 579 String type = deployParams.getTargetFormat(); 580 String bundleType = (appImage ? "IMAGE" : "INSTALLER"); 581 jdk.jpackage.internal.Bundler savedBundler = null; 582 583 for (jdk.jpackage.internal.Bundler bundler : 584 Bundlers.createBundlersInstance().getBundlers(bundleType)) { 585 if (bundler.supported(runtimeInstaller)) { 586 if (type == null) { 587 if (bundler.isDefault()) { 588 return bundler; 589 } else { 590 savedBundler = bundler; 591 } 592 } else if (appImage || 593 type.equalsIgnoreCase(bundler.getID())) { 594 return bundler; 595 } 596 } 597 } 598 return savedBundler; 599 } 600 601 private void generateBundle(Map<String,? super Object> params) 602 throws PackagerException { 603 604 boolean bundleCreated = false; 605 606 // the temp-root needs to be fetched from the params early, 607 // to prevent each copy of the params (such as may be used for 608 // additional launchers) from generating a separate temp-root when 609 // the default is used (the default is a new temp directory) 610 // The bundler.cleanup() below would not otherwise be able to 611 // clean these extra (and unneeded) temp directories. 612 StandardBundlerParam.TEMP_ROOT.fetchFrom(params); 613 614 // determine what bundler to run 615 jdk.jpackage.internal.Bundler bundler = getPlatformBundler(); 616 617 if (bundler == null) { 618 throw new PackagerException("ERR_InvalidInstallerType", 619 deployParams.getTargetFormat()); 620 } 621 622 Map<String, ? super Object> localParams = new HashMap<>(params); 623 try { 624 bundler.validate(localParams); 625 File result = bundler.execute(localParams, deployParams.outdir); 626 if (result == null) { 627 throw new PackagerException("MSG_BundlerFailed", 628 bundler.getID(), bundler.getName()); 629 } 630 Log.verbose(MessageFormat.format( 631 I18N.getString("message.bundle-created"), 632 bundler.getName())); 633 } catch (ConfigException e) { 634 Log.verbose(e); 635 if (e.getAdvice() != null) { 636 throw new PackagerException(e, "MSG_BundlerConfigException", 637 bundler.getName(), e.getMessage(), e.getAdvice()); 638 } else { 639 throw new PackagerException(e, 640 "MSG_BundlerConfigExceptionNoAdvice", 641 bundler.getName(), e.getMessage()); 642 } 643 } catch (RuntimeException re) { 644 Log.verbose(re); 645 throw new PackagerException(re, "MSG_BundlerRuntimeException", 646 bundler.getName(), re.toString()); 647 } finally { 648 if (userProvidedBuildRoot) { 649 Log.verbose(MessageFormat.format( 650 I18N.getString("message.debug-working-directory"), 651 (new File(buildRoot)).getAbsolutePath())); 652 } else { 653 // always clean up the temporary directory created 654 // when --temp-root option not used. 655 bundler.cleanup(localParams); 656 } 657 } 658 } 659 660 private void addResources(DeployParams deployParams, 661 String inputdir, String mainJar) throws PackagerException { 662 663 if (inputdir == null || inputdir.isEmpty()) { 664 return; 665 } 666 667 File baseDir = new File(inputdir); 668 669 if (!baseDir.isDirectory()) { 670 throw new PackagerException("ERR_InputNotDirectory", inputdir); 671 } 672 if (!baseDir.canRead()) { 673 throw new PackagerException("ERR_CannotReadInputDir", inputdir); 674 } 675 676 List<String> fileNames; 677 fileNames = new ArrayList<>(); 678 try (Stream<Path> files = Files.list(baseDir.toPath())) { 679 files.forEach(file -> fileNames.add( 680 file.getFileName().toString())); 681 } catch (IOException e) { 682 Log.error("Unable to add resources: " + e.getMessage()); 683 } 684 fileNames.forEach(file -> deployParams.addResource(baseDir, file)); 685 686 deployParams.setClasspath(mainJar); 687 } 688 689 static CLIOptions toCLIOption(String arg) { 690 CLIOptions option; 691 if ((option = argIds.get(arg)) == null) { 692 option = argShortIds.get(arg); 693 } 694 return option; 695 } 696 697 static Map<String, String> getPropertiesFromFile(String filename) { 698 Map<String, String> map = new HashMap<>(); 699 // load properties file 700 File file = new File(filename); 701 Properties properties = new Properties(); 702 try (FileInputStream in = new FileInputStream(file)) { 703 properties.load(in); 704 } catch (IOException e) { 705 Log.error("Exception: " + e.getMessage()); 706 } 707 708 for (final String name: properties.stringPropertyNames()) { 709 map.put(name, properties.getProperty(name)); 710 } 711 712 return map; 713 } 714 715 static List<String> getArgumentList(String inputString) { 716 List<String> list = new ArrayList<>(); 717 if (inputString == null || inputString.isEmpty()) { 718 return list; 719 } 720 721 // The "pattern" regexp attempts to abide to the rule that 722 // strings are delimited by whitespace unless surrounded by 723 // quotes, then it is anything (including spaces) in the quotes. 724 Matcher m = pattern.matcher(inputString); 725 while (m.find()) { 726 String s = inputString.substring(m.start(), m.end()).trim(); 727 // Ensure we do not have an empty string. trim() will take care of 728 // whitespace only strings. The regex preserves quotes and escaped 729 // chars so we need to clean them before adding to the List 730 if (!s.isEmpty()) { 731 list.add(unquoteIfNeeded(s)); 732 } 733 } 734 return list; 735 } 736 737 private static String unquoteIfNeeded(String in) { 738 if (in == null) { 739 return null; 740 } 741 742 if (in.isEmpty()) { 743 return ""; 744 } 745 746 // Use code points to preserve non-ASCII chars 747 StringBuilder sb = new StringBuilder(); 748 int codeLen = in.codePointCount(0, in.length()); 749 int quoteChar = -1; 750 for (int i = 0; i < codeLen; i++) { 751 int code = in.codePointAt(i); 752 if (code == '"' || code == '\'') { 753 // If quote is escaped make sure to copy it 754 if (i > 0 && in.codePointAt(i - 1) == '\\') { 755 sb.deleteCharAt(sb.length() - 1); 756 sb.appendCodePoint(code); 757 continue; 758 } 759 if (quoteChar != -1) { 760 if (code == quoteChar) { 761 // close quote, skip char 762 quoteChar = -1; 763 } else { 764 sb.appendCodePoint(code); 765 } 766 } else { 767 // opening quote, skip char 768 quoteChar = code; 769 } 770 } else { 771 sb.appendCodePoint(code); 772 } 773 } 774 return sb.toString(); 775 } 776 777 private String getMainClassFromManifest() { 778 if (mainJarPath == null || 779 input == null ) { 780 return null; 781 } 782 783 JarFile jf; 784 try { 785 File file = new File(input, mainJarPath); 786 if (!file.exists()) { 787 return null; 788 } 789 jf = new JarFile(file); 790 Manifest m = jf.getManifest(); 791 Attributes attrs = (m != null) ? m.getMainAttributes() : null; 792 if (attrs != null) { 793 return attrs.getValue(Attributes.Name.MAIN_CLASS); 794 } 795 } catch (IOException ignore) {} 796 return null; 797 } 798 799 }