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