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