1 /* 2 * Copyright (c) 2015, 2016, 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.tools.jlink.internal; 26 27 import java.io.File; 28 import java.io.IOException; 29 import java.io.PrintWriter; 30 import java.lang.module.Configuration; 31 import java.lang.module.ModuleFinder; 32 import java.lang.reflect.Layer; 33 import java.nio.file.Files; 34 import java.nio.file.Path; 35 import java.nio.file.Paths; 36 import java.text.MessageFormat; 37 import java.util.ArrayList; 38 import java.util.Arrays; 39 import java.util.Collections; 40 import java.util.HashMap; 41 import java.util.HashSet; 42 import java.util.List; 43 import java.util.Locale; 44 import java.util.Map; 45 import java.util.Map.Entry; 46 import java.util.MissingResourceException; 47 import java.util.ResourceBundle; 48 import java.util.Set; 49 50 import jdk.internal.module.ConfigurableModuleFinder; 51 import jdk.internal.module.ConfigurableModuleFinder.Phase; 52 import jdk.tools.jlink.Jlink; 53 import jdk.tools.jlink.Jlink.PluginsConfiguration; 54 import jdk.tools.jlink.plugin.Plugin; 55 import jdk.tools.jlink.plugin.Plugin.Category; 56 import jdk.tools.jlink.builder.DefaultImageBuilder; 57 import jdk.tools.jlink.builder.ImageBuilder; 58 import jdk.tools.jlink.plugin.PluginException; 59 import jdk.tools.jlink.internal.plugins.PluginsResourceBundle; 60 import jdk.tools.jlink.internal.plugins.DefaultCompressPlugin; 61 import jdk.tools.jlink.internal.plugins.StripDebugPlugin; 62 63 /** 64 * 65 * JLink and JImage tools shared helper. 66 */ 67 public final class TaskHelper { 68 69 public static final String JLINK_BUNDLE = "jdk.tools.jlink.resources.jlink"; 70 public static final String JIMAGE_BUNDLE = "jdk.tools.jimage.resources.jimage"; 71 72 private static final String DEFAULTS_PROPERTY = "jdk.jlink.defaults"; 73 74 public final class BadArgs extends Exception { 75 76 static final long serialVersionUID = 8765093759964640721L; 77 78 private BadArgs(String key, Object... args) { 79 super(bundleHelper.getMessage(key, args)); 80 this.key = key; 81 this.args = args; 82 } 83 84 public BadArgs showUsage(boolean b) { 85 showUsage = b; 86 return this; 87 } 88 public final String key; 89 public final Object[] args; 90 public boolean showUsage; 91 } 92 93 public static class Option<T> { 94 95 public interface Processing<T> { 96 97 void process(T task, String opt, String arg) throws BadArgs; 98 } 99 100 final boolean hasArg; 101 final Processing<T> processing; 102 final boolean hidden; 103 final String[] aliases; 104 105 public Option(boolean hasArg, Processing<T> processing, boolean hidden, String... aliases) { 106 this.hasArg = hasArg; 107 this.processing = processing; 108 this.aliases = aliases; 109 this.hidden = hidden; 110 } 111 112 public Option(boolean hasArg, Processing<T> processing, String... aliases) { 113 this(hasArg, processing, false, aliases); 114 } 115 116 public boolean isHidden() { 117 return hidden; 118 } 119 120 public boolean matches(String opt) { 121 for (String a : aliases) { 122 if (a.equals(opt)) { 123 return true; 124 } else if (opt.startsWith("--") 125 && (hasArg && opt.startsWith(a + "="))) { 126 return true; 127 } 128 } 129 return false; 130 } 131 132 public boolean ignoreRest() { 133 return false; 134 } 135 136 void process(T task, String opt, String arg) throws BadArgs { 137 processing.process(task, opt, arg); 138 } 139 140 public String[] aliases() { 141 return aliases; 142 } 143 } 144 145 private static class PlugOption extends Option<PluginsOptions> { 146 147 public PlugOption(boolean hasArg, 148 Processing<PluginsOptions> processing, boolean hidden, String... aliases) { 149 super(hasArg, processing, hidden, aliases); 150 } 151 152 public PlugOption(boolean hasArg, 153 Processing<PluginsOptions> processing, String... aliases) { 154 super(hasArg, processing, aliases); 155 } 156 } 157 158 private final class PluginsOptions { 159 160 private static final String PLUGINS_PATH = "--plugin-module-path"; 161 private static final String POST_PROCESS = "--post-process-path"; 162 163 private Layer pluginsLayer = Layer.boot(); 164 private String lastSorter; 165 private boolean listPlugins; 166 private Path existingImage; 167 168 // plugin to args maps. Each plugin may be used more than once in command line. 169 // Each such occurrence results in a Map of arguments. So, there could be multiple 170 // args maps per plugin instance. 171 private final Map<Plugin, List<Map<String, String>>> pluginToMaps = new HashMap<>(); 172 private final List<PlugOption> pluginsOptions = new ArrayList<>(); 173 private final List<PlugOption> mainOptions = new ArrayList<>(); 174 175 private PluginsOptions(String pp) throws BadArgs { 176 177 if (pp != null) { 178 String[] dirs = pp.split(File.pathSeparator); 179 List<Path> paths = new ArrayList<>(dirs.length); 180 for (String dir : dirs) { 181 paths.add(Paths.get(dir)); 182 } 183 184 pluginsLayer = createPluginsLayer(paths); 185 } 186 187 Set<String> optionsSeen = new HashSet<>(); 188 for (Plugin plugin : PluginRepository. 189 getPlugins(pluginsLayer)) { 190 if (!Utils.isDisabled(plugin)) { 191 addOrderedPluginOptions(plugin, optionsSeen); 192 } 193 } 194 mainOptions.add(new PlugOption(false, 195 (task, opt, arg) -> { 196 // This option is handled prior 197 // to have the options parsed. 198 }, 199 "--plugin-module-path")); 200 mainOptions.add(new PlugOption(true, (task, opt, arg) -> { 201 Path path = Paths.get(arg); 202 if (!Files.exists(path) || !Files.isDirectory(path)) { 203 throw newBadArgs("err.existing.image.must.exist"); 204 } 205 existingImage = path.toAbsolutePath(); 206 }, true, POST_PROCESS)); 207 mainOptions.add(new PlugOption(true, 208 (task, opt, arg) -> { 209 lastSorter = arg; 210 }, 211 true, "--resources-last-sorter")); 212 mainOptions.add(new PlugOption(false, 213 (task, opt, arg) -> { 214 listPlugins = true; 215 }, 216 "--list-plugins")); 217 } 218 219 private List<Map<String, String>> argListFor(Plugin plugin) { 220 List<Map<String, String>> mapList = pluginToMaps.get(plugin); 221 if (mapList == null) { 222 mapList = new ArrayList<>(); 223 pluginToMaps.put(plugin, mapList); 224 } 225 return mapList; 226 } 227 228 private void addEmptyArgumentMap(Plugin plugin) { 229 argListFor(plugin).add(Collections.emptyMap()); 230 } 231 232 private Map<String, String> addArgumentMap(Plugin plugin) { 233 Map<String, String> map = new HashMap<>(); 234 argListFor(plugin).add(map); 235 return map; 236 } 237 238 private void addOrderedPluginOptions(Plugin plugin, 239 Set<String> optionsSeen) throws BadArgs { 240 String option = plugin.getOption(); 241 if (option == null) { 242 return; 243 } 244 245 // make sure that more than one plugin does not use the same option! 246 if (optionsSeen.contains(option)) { 247 throw new BadArgs("err.plugin.mutiple.options", 248 option); 249 } 250 optionsSeen.add(option); 251 252 PlugOption plugOption 253 = new PlugOption(plugin.hasArguments(), 254 (task, opt, arg) -> { 255 if (!Utils.isFunctional(plugin)) { 256 throw newBadArgs("err.provider.not.functional", 257 option); 258 } 259 260 if (! plugin.hasArguments()) { 261 addEmptyArgumentMap(plugin); 262 return; 263 } 264 265 Map<String, String> m = addArgumentMap(plugin); 266 // handle one or more arguments 267 if (arg.indexOf(':') == -1) { 268 // single argument case 269 m.put(option, arg); 270 } else { 271 // This option can accept more than one arguments 272 // like --option_name=arg_value:arg2=value2:arg3=value3 273 274 // ":" followed by word char condition takes care of args that 275 // like Windows absolute paths "C:\foo", "C:/foo" [cygwin] etc. 276 // This enforces that key names start with a word character. 277 String[] args = arg.split(":(?=\\w)", -1); 278 String firstArg = args[0]; 279 if (firstArg.isEmpty()) { 280 throw newBadArgs("err.provider.additional.arg.error", 281 option, arg); 282 } 283 m.put(option, firstArg); 284 // process the additional arguments 285 for (int i = 1; i < args.length; i++) { 286 String addArg = args[i]; 287 int eqIdx = addArg.indexOf('='); 288 if (eqIdx == -1) { 289 throw newBadArgs("err.provider.additional.arg.error", 290 option, arg); 291 } 292 293 String addArgName = addArg.substring(0, eqIdx); 294 String addArgValue = addArg.substring(eqIdx+1); 295 if (addArgName.isEmpty() || addArgValue.isEmpty()) { 296 throw newBadArgs("err.provider.additional.arg.error", 297 option, arg); 298 } 299 m.put(addArgName, addArgValue); 300 } 301 } 302 }, 303 "--" + option); 304 pluginsOptions.add(plugOption); 305 306 if (Utils.isFunctional(plugin)) { 307 if (Utils.isAutoEnabled(plugin)) { 308 addEmptyArgumentMap(plugin); 309 pluginsOptions.add( 310 new PlugOption(false, 311 (task, opt, arg) -> { 312 pluginToMaps.remove(plugin); 313 }, "--disable-" + plugin.getName()) 314 ); 315 } 316 317 if (plugin instanceof DefaultCompressPlugin) { 318 plugOption 319 = new PlugOption(false, 320 (task, opt, arg) -> { 321 Map<String, String> m = addArgumentMap(plugin); 322 m.put(DefaultCompressPlugin.NAME, DefaultCompressPlugin.LEVEL_2); 323 }, "-c"); 324 mainOptions.add(plugOption); 325 } else if (plugin instanceof StripDebugPlugin) { 326 plugOption 327 = new PlugOption(false, 328 (task, opt, arg) -> { 329 addArgumentMap(plugin); 330 }, "-G"); 331 mainOptions.add(plugOption); 332 } 333 } 334 } 335 336 private PlugOption getOption(String name) throws BadArgs { 337 for (PlugOption o : pluginsOptions) { 338 if (o.matches(name)) { 339 return o; 340 } 341 } 342 for (PlugOption o : mainOptions) { 343 if (o.matches(name)) { 344 return o; 345 } 346 } 347 return null; 348 } 349 350 private PluginsConfiguration getPluginsConfig(Path output 351 ) throws IOException, BadArgs { 352 if (output != null) { 353 if (Files.exists(output)) { 354 throw new PluginException(PluginsResourceBundle. 355 getMessage("err.dir.already.exits", output)); 356 } 357 } 358 359 List<Plugin> pluginsList = new ArrayList<>(); 360 for (Entry<Plugin, List<Map<String, String>>> entry : pluginToMaps.entrySet()) { 361 Plugin plugin = entry.getKey(); 362 List<Map<String, String>> argsMaps = entry.getValue(); 363 364 // same plugin option may be used multiple times in command line. 365 // we call configure once for each occurrence. It is upto the plugin 366 // to 'merge' and/or 'override' arguments. 367 for (Map<String, String> map : argsMaps) { 368 plugin.configure(Collections.unmodifiableMap(map)); 369 } 370 371 if (!Utils.isDisabled(plugin)) { 372 pluginsList.add(plugin); 373 } 374 } 375 376 // recreate or postprocessing don't require an output directory. 377 ImageBuilder builder = null; 378 if (output != null) { 379 builder = new DefaultImageBuilder(output); 380 381 } 382 return new Jlink.PluginsConfiguration(pluginsList, 383 builder, lastSorter); 384 } 385 } 386 387 private static final class ResourceBundleHelper { 388 389 private final ResourceBundle bundle; 390 private final ResourceBundle pluginBundle; 391 392 ResourceBundleHelper(String path) { 393 Locale locale = Locale.getDefault(); 394 try { 395 bundle = ResourceBundle.getBundle(path, locale); 396 pluginBundle = ResourceBundle.getBundle("jdk.tools.jlink.resources.plugins", locale); 397 } catch (MissingResourceException e) { 398 throw new InternalError("Cannot find jlink resource bundle for locale " + locale); 399 } 400 } 401 402 String getMessage(String key, Object... args) { 403 String val; 404 try { 405 val = bundle.getString(key); 406 } catch (MissingResourceException e) { 407 // XXX OK, check in plugin bundle 408 val = pluginBundle.getString(key); 409 } 410 return MessageFormat.format(val, args); 411 } 412 413 } 414 415 public final class OptionsHelper<T> { 416 417 private final List<Option<T>> options; 418 private String[] command; 419 private String defaults; 420 421 OptionsHelper(List<Option<T>> options) { 422 this.options = options; 423 } 424 425 private boolean hasArgument(String optionName) throws BadArgs { 426 Option<?> opt = getOption(optionName); 427 if (opt == null) { 428 opt = pluginOptions.getOption(optionName); 429 if (opt == null) { 430 throw new BadArgs("err.unknown.option", optionName). 431 showUsage(true); 432 } 433 } 434 return opt.hasArg; 435 } 436 437 public boolean shouldListPlugins() { 438 return pluginOptions.listPlugins; 439 } 440 441 private String getPluginsPath(String[] args) throws BadArgs { 442 String pp = null; 443 for (int i = 0; i < args.length; i++) { 444 if (args[i].equals(PluginsOptions.PLUGINS_PATH)) { 445 if (i == args.length - 1) { 446 throw new BadArgs("err.no.plugins.path").showUsage(true); 447 } else { 448 i += 1; 449 pp = args[i]; 450 if (!pp.isEmpty() && pp.charAt(0) == '-') { 451 throw new BadArgs("err.no.plugins.path").showUsage(true); 452 } 453 break; 454 } 455 } 456 } 457 return pp; 458 } 459 460 public List<String> handleOptions(T task, String[] args) throws BadArgs { 461 // findbugs warning, copy instead of keeping a reference. 462 command = Arrays.copyOf(args, args.length); 463 464 // Must extract it prior to do any option analysis. 465 // Required to interpret custom plugin options. 466 // Unit tests can call Task multiple time in same JVM. 467 pluginOptions = new PluginsOptions(getPluginsPath(args)); 468 469 // First extract plugins path if any 470 String pp = null; 471 List<String> filteredArgs = new ArrayList<>(); 472 for (int i = 0; i < args.length; i++) { 473 if (args[i].equals(PluginsOptions.PLUGINS_PATH)) { 474 if (i == args.length - 1) { 475 throw new BadArgs("err.no.plugins.path").showUsage(true); 476 } else { 477 warning("warn.thirdparty.plugins.enabled"); 478 log.println(bundleHelper.getMessage("warn.thirdparty.plugins")); 479 i += 1; 480 String arg = args[i]; 481 if (!arg.isEmpty() && arg.charAt(0) == '-') { 482 throw new BadArgs("err.no.plugins.path").showUsage(true); 483 } 484 pp = args[i]; 485 } 486 } else { 487 filteredArgs.add(args[i]); 488 } 489 } 490 String[] arr = new String[filteredArgs.size()]; 491 args = filteredArgs.toArray(arr); 492 493 List<String> rest = new ArrayList<>(); 494 // process options 495 for (int i = 0; i < args.length; i++) { 496 if (!args[i].isEmpty() && args[i].charAt(0) == '-') { 497 String name = args[i]; 498 PlugOption pluginOption = null; 499 Option<T> option = getOption(name); 500 if (option == null) { 501 pluginOption = pluginOptions.getOption(name); 502 if (pluginOption == null) { 503 504 throw new BadArgs("err.unknown.option", name). 505 showUsage(true); 506 } 507 } 508 Option<?> opt = pluginOption == null ? option : pluginOption; 509 String param = null; 510 if (opt.hasArg) { 511 if (name.startsWith("--") && name.indexOf('=') > 0) { 512 param = name.substring(name.indexOf('=') + 1, 513 name.length()); 514 } else if (i + 1 < args.length) { 515 param = args[++i]; 516 } 517 if (param == null || param.isEmpty() 518 || (param.length() >= 2 && param.charAt(0) == '-' 519 && param.charAt(1) == '-')) { 520 throw new BadArgs("err.missing.arg", name). 521 showUsage(true); 522 } 523 } 524 if (pluginOption != null) { 525 pluginOption.process(pluginOptions, name, param); 526 } else { 527 option.process(task, name, param); 528 } 529 if (opt.ignoreRest()) { 530 i = args.length; 531 } 532 } else { 533 rest.add(args[i]); 534 } 535 } 536 return rest; 537 } 538 539 private Option<T> getOption(String name) { 540 for (Option<T> o : options) { 541 if (o.matches(name)) { 542 return o; 543 } 544 } 545 return null; 546 } 547 548 public void showHelp(String progName) { 549 showHelp(progName, true); 550 } 551 552 private void showHelp(String progName, boolean showsImageBuilder) { 553 log.println(bundleHelper.getMessage("main.usage", progName)); 554 for (Option<?> o : options) { 555 String name = o.aliases[0].substring(1); // there must always be at least one name 556 name = name.charAt(0) == '-' ? name.substring(1) : name; 557 if (o.isHidden() || name.equals("h")) { 558 continue; 559 } 560 log.println(bundleHelper.getMessage("main.opt." + name)); 561 } 562 563 for (Option<?> o : pluginOptions.mainOptions) { 564 if (o.aliases[0].equals(PluginsOptions.POST_PROCESS) 565 && !showsImageBuilder) { 566 continue; 567 } 568 String name = o.aliases[0].substring(1); // there must always be at least one name 569 name = name.charAt(0) == '-' ? name.substring(1) : name; 570 if (o.isHidden()) { 571 continue; 572 } 573 log.println(bundleHelper.getMessage("plugin.opt." + name)); 574 } 575 576 log.println(bundleHelper.getMessage("main.command.files")); 577 } 578 579 public void listPlugins() { 580 log.println("\n" + bundleHelper.getMessage("main.extended.help")); 581 List<Plugin> pluginList = PluginRepository. 582 getPlugins(pluginOptions.pluginsLayer); 583 for (Plugin plugin : Utils.getSortedPlugins(pluginList)) { 584 showPlugin(plugin, log); 585 } 586 587 log.println("\n" + bundleHelper.getMessage("main.extended.help.footer")); 588 } 589 590 private void showPlugin(Plugin plugin, PrintWriter log) { 591 if (showsPlugin(plugin)) { 592 log.println("\n" + bundleHelper.getMessage("main.plugin.name") 593 + ": " + plugin.getName()); 594 595 // print verbose details for non-builtin plugins 596 if (!Utils.isBuiltin(plugin)) { 597 log.println(bundleHelper.getMessage("main.plugin.class") 598 + ": " + plugin.getClass().getName()); 599 log.println(bundleHelper.getMessage("main.plugin.module") 600 + ": " + plugin.getClass().getModule().getName()); 601 Category category = plugin.getType(); 602 log.println(bundleHelper.getMessage("main.plugin.category") 603 + ": " + category.getName()); 604 log.println(bundleHelper.getMessage("main.plugin.state") 605 + ": " + plugin.getStateDescription()); 606 } 607 608 String option = plugin.getOption(); 609 if (option != null) { 610 log.println(bundleHelper.getMessage("main.plugin.option") 611 + ": --" + plugin.getOption() 612 + (plugin.hasArguments()? ("=" + plugin.getArgumentsDescription()) : "")); 613 } 614 615 // description can be long spanning more than one line and so 616 // print a newline after description label. 617 log.println(bundleHelper.getMessage("main.plugin.description") 618 + ": " + plugin.getDescription()); 619 } 620 } 621 622 String[] getInputCommand() { 623 return command; 624 } 625 626 String getDefaults() { 627 return defaults; 628 } 629 630 public Layer getPluginsLayer() { 631 return pluginOptions.pluginsLayer; 632 } 633 } 634 635 private PluginsOptions pluginOptions; 636 private PrintWriter log; 637 private final ResourceBundleHelper bundleHelper; 638 639 public TaskHelper(String path) { 640 if (!JLINK_BUNDLE.equals(path) && !JIMAGE_BUNDLE.equals(path)) { 641 throw new IllegalArgumentException("Invalid Bundle"); 642 } 643 this.bundleHelper = new ResourceBundleHelper(path); 644 } 645 646 public <T> OptionsHelper<T> newOptionsHelper(Class<T> clazz, 647 Option<?>[] options) { 648 List<Option<T>> optionsList = new ArrayList<>(); 649 for (Option<?> o : options) { 650 @SuppressWarnings("unchecked") 651 Option<T> opt = (Option<T>) o; 652 optionsList.add(opt); 653 } 654 return new OptionsHelper<>(optionsList); 655 } 656 657 public BadArgs newBadArgs(String key, Object... args) { 658 return new BadArgs(key, args); 659 } 660 661 public String getMessage(String key, Object... args) { 662 return bundleHelper.getMessage(key, args); 663 } 664 665 public void setLog(PrintWriter log) { 666 this.log = log; 667 } 668 669 public void reportError(String key, Object... args) { 670 log.println(bundleHelper.getMessage("error.prefix") + " " 671 + bundleHelper.getMessage(key, args)); 672 } 673 674 public void reportUnknownError(String message) { 675 log.println(bundleHelper.getMessage("error.prefix") + " " + message); 676 } 677 678 public void warning(String key, Object... args) { 679 log.println(bundleHelper.getMessage("warn.prefix") + " " 680 + bundleHelper.getMessage(key, args)); 681 } 682 683 public PluginsConfiguration getPluginsConfig(Path output) 684 throws IOException, BadArgs { 685 return pluginOptions.getPluginsConfig(output); 686 } 687 688 public Path getExistingImage() { 689 return pluginOptions.existingImage; 690 } 691 692 public void showVersion(boolean full) { 693 log.println(version(full ? "full" : "release")); 694 } 695 696 public String version(String key) { 697 return System.getProperty("java.version"); 698 } 699 700 static Layer createPluginsLayer(List<Path> paths) { 701 Path[] arr = new Path[paths.size()]; 702 paths.toArray(arr); 703 ModuleFinder finder = ModuleFinder.of(arr); 704 705 // jmods are located at link-time 706 if (finder instanceof ConfigurableModuleFinder) { 707 ((ConfigurableModuleFinder) finder).configurePhase(Phase.LINK_TIME); 708 } 709 710 Configuration bootConfiguration = Layer.boot().configuration(); 711 try { 712 Configuration cf = bootConfiguration 713 .resolveRequiresAndUses(ModuleFinder.of(), 714 finder, 715 Collections.emptySet()); 716 ClassLoader scl = ClassLoader.getSystemClassLoader(); 717 return Layer.boot().defineModulesWithOneLoader(cf, scl); 718 } catch (Exception ex) { 719 // Malformed plugin modules (e.g.: same package in multiple modules). 720 throw new PluginException("Invalid modules in the plugins path: " + ex); 721 } 722 } 723 724 // Display all plugins 725 private static boolean showsPlugin(Plugin plugin) { 726 return (!Utils.isDisabled(plugin) && plugin.getOption() != null); 727 } 728 }