/* * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package jdk.tools.jlink.internal; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.lang.module.Configuration; import java.lang.module.ModuleFinder; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.tools.jlink.internal.plugins.ExcludeJmodSectionPlugin; import jdk.tools.jlink.plugin.Plugin; import jdk.tools.jlink.plugin.Plugin.Category; import jdk.tools.jlink.builder.DefaultImageBuilder; import jdk.tools.jlink.builder.ImageBuilder; import jdk.tools.jlink.plugin.PluginException; import jdk.tools.jlink.internal.Jlink.PluginsConfiguration; import jdk.tools.jlink.internal.plugins.PluginsResourceBundle; import jdk.tools.jlink.internal.plugins.DefaultCompressPlugin; import jdk.tools.jlink.internal.plugins.StripDebugPlugin; import jdk.internal.module.ModulePath; /** * * JLink and JImage tools shared helper. */ public final class TaskHelper { public static final String JLINK_BUNDLE = "jdk.tools.jlink.resources.jlink"; public static final String JIMAGE_BUNDLE = "jdk.tools.jimage.resources.jimage"; private static final String DEFAULTS_PROPERTY = "jdk.jlink.defaults"; public final class BadArgs extends Exception { static final long serialVersionUID = 8765093759964640721L; private BadArgs(String key, Object... args) { super(bundleHelper.getMessage(key, args)); this.key = key; this.args = args; } public BadArgs showUsage(boolean b) { showUsage = b; return this; } public final String key; public final Object[] args; public boolean showUsage; } public static class Option implements Comparable { public interface Processing { void process(T task, String opt, String arg) throws BadArgs; } final boolean hasArg; final Processing processing; final boolean hidden; final String name; final String shortname; final String shortname2; final boolean terminalOption; public Option(boolean hasArg, Processing processing, boolean hidden, String name, String shortname, String shortname2, boolean isTerminal) { if (!name.startsWith("--")) { throw new RuntimeException("option name missing --, " + name); } if (!shortname.isEmpty() && !shortname.startsWith("-")) { throw new RuntimeException("short name missing -, " + shortname); } this.hasArg = hasArg; this.processing = processing; this.hidden = hidden; this.name = name; this.shortname = shortname; this.shortname2 = shortname2; this.terminalOption = isTerminal; } public Option(boolean hasArg, Processing processing, boolean hidden, String name, String shortname, boolean isTerminal) { this(hasArg, processing, false, name, shortname, "", isTerminal); } public Option(boolean hasArg, Processing processing, String name, String shortname, boolean isTerminal) { this(hasArg, processing, false, name, shortname, "", isTerminal); } public Option(boolean hasArg, Processing processing, String name, String shortname, String shortname2) { this(hasArg, processing, false, name, shortname, shortname2, false); } public Option(boolean hasArg, Processing processing, String name, String shortname) { this(hasArg, processing, false, name, shortname, "", false); } public Option(boolean hasArg, Processing processing, boolean hidden, String name) { this(hasArg, processing, hidden, name, "", "", false); } public Option(boolean hasArg, Processing processing, String name) { this(hasArg, processing, false, name, "", false); } public boolean isHidden() { return hidden; } public boolean isTerminal() { return terminalOption; } public boolean matches(String opt) { return opt.equals(name) || opt.equals(shortname) || opt.equals(shortname2) || hasArg && opt.startsWith("--") && opt.startsWith(name + "="); } public boolean ignoreRest() { return false; } void process(T task, String opt, String arg) throws BadArgs { processing.process(task, opt, arg); } public String getName() { return name; } public String resourceName() { return resourcePrefix() + name.substring(2); } public String getShortname() { return shortname; } public String resourcePrefix() { return "main.opt."; } @Override public int compareTo(Object object) { if (!(object instanceof Option)) { throw new RuntimeException("comparing non-Option"); } Option option = (Option)object; return name.compareTo(option.name); } } private static class PluginOption extends Option { public PluginOption(boolean hasArg, Processing processing, boolean hidden, String name, String shortname) { super(hasArg, processing, hidden, name, shortname, false); } public PluginOption(boolean hasArg, Processing processing, boolean hidden, String name) { super(hasArg, processing, hidden, name, "", false); } public String resourcePrefix() { return "plugin.opt."; } } private final class PluginsHelper { private ModuleLayer pluginsLayer = ModuleLayer.boot(); private final List plugins; private String lastSorter; private boolean listPlugins; private Path existingImage; // plugin to args maps. Each plugin may be used more than once in command line. // Each such occurrence results in a Map of arguments. So, there could be multiple // args maps per plugin instance. private final Map>> pluginToMaps = new HashMap<>(); private final List pluginsOptions = new ArrayList<>(); private final List mainOptions = new ArrayList<>(); private PluginsHelper(String pp) throws BadArgs { if (pp != null) { String[] dirs = pp.split(File.pathSeparator); List paths = new ArrayList<>(dirs.length); for (String dir : dirs) { paths.add(Paths.get(dir)); } pluginsLayer = createPluginsLayer(paths); } plugins = PluginRepository.getPlugins(pluginsLayer); Set optionsSeen = new HashSet<>(); for (Plugin plugin : plugins) { if (!Utils.isDisabled(plugin)) { addOrderedPluginOptions(plugin, optionsSeen); } } mainOptions.add(new PluginOption(true, (task, opt, arg) -> { for (Plugin plugin : plugins) { if (plugin.getName().equals(arg)) { pluginToMaps.remove(plugin); return; } } throw newBadArgs("err.no.such.plugin", arg); }, false, "--disable-plugin")); mainOptions.add(new PluginOption(true, (task, opt, arg) -> { Path path = Paths.get(arg); if (!Files.exists(path) || !Files.isDirectory(path)) { throw newBadArgs("err.image.must.exist", path); } existingImage = path.toAbsolutePath(); }, true, "--post-process-path")); mainOptions.add(new PluginOption(true, (task, opt, arg) -> { lastSorter = arg; }, true, "--resources-last-sorter")); mainOptions.add(new PluginOption(false, (task, opt, arg) -> { listPlugins = true; }, false, "--list-plugins")); } private List> argListFor(Plugin plugin) { List> mapList = pluginToMaps.get(plugin); if (mapList == null) { mapList = new ArrayList<>(); pluginToMaps.put(plugin, mapList); } return mapList; } private void addEmptyArgumentMap(Plugin plugin) { argListFor(plugin).add(Collections.emptyMap()); } private Map addArgumentMap(Plugin plugin) { Map map = new HashMap<>(); argListFor(plugin).add(map); return map; } private void addOrderedPluginOptions(Plugin plugin, Set optionsSeen) throws BadArgs { String option = plugin.getOption(); if (option == null) { return; } // make sure that more than one plugin does not use the same option! if (optionsSeen.contains(option)) { throw new BadArgs("err.plugin.mutiple.options", option); } optionsSeen.add(option); PluginOption plugOption = new PluginOption(plugin.hasArguments(), (task, opt, arg) -> { if (!Utils.isFunctional(plugin)) { throw newBadArgs("err.provider.not.functional", option); } if (! plugin.hasArguments()) { addEmptyArgumentMap(plugin); return; } Map m = addArgumentMap(plugin); // handle one or more arguments if (arg.indexOf(':') == -1) { // single argument case m.put(option, arg); } else { // This option can accept more than one arguments // like --option_name=arg_value:arg2=value2:arg3=value3 // ":" followed by word char condition takes care of args that // like Windows absolute paths "C:\foo", "C:/foo" [cygwin] etc. // This enforces that key names start with a word character. String[] args = arg.split(":(?=\\w)", -1); String firstArg = args[0]; if (firstArg.isEmpty()) { throw newBadArgs("err.provider.additional.arg.error", option, arg); } m.put(option, firstArg); // process the additional arguments for (int i = 1; i < args.length; i++) { String addArg = args[i]; int eqIdx = addArg.indexOf('='); if (eqIdx == -1) { throw newBadArgs("err.provider.additional.arg.error", option, arg); } String addArgName = addArg.substring(0, eqIdx); String addArgValue = addArg.substring(eqIdx+1); if (addArgName.isEmpty() || addArgValue.isEmpty()) { throw newBadArgs("err.provider.additional.arg.error", option, arg); } m.put(addArgName, addArgValue); } } }, false, "--" + option); pluginsOptions.add(plugOption); if (Utils.isFunctional(plugin)) { if (Utils.isAutoEnabled(plugin)) { addEmptyArgumentMap(plugin); } if (plugin instanceof DefaultCompressPlugin) { plugOption = new PluginOption(false, (task, opt, arg) -> { Map m = addArgumentMap(plugin); m.put(DefaultCompressPlugin.NAME, DefaultCompressPlugin.LEVEL_2); }, false, "--compress", "-c"); mainOptions.add(plugOption); } else if (plugin instanceof StripDebugPlugin) { plugOption = new PluginOption(false, (task, opt, arg) -> { addArgumentMap(plugin); }, false, "--strip-debug", "-G"); mainOptions.add(plugOption); } else if (plugin instanceof ExcludeJmodSectionPlugin) { plugOption = new PluginOption(false, (task, opt, arg) -> { Map m = addArgumentMap(plugin); m.put(ExcludeJmodSectionPlugin.NAME, ExcludeJmodSectionPlugin.MAN_PAGES); }, false, "--no-man-pages"); mainOptions.add(plugOption); plugOption = new PluginOption(false, (task, opt, arg) -> { Map m = addArgumentMap(plugin); m.put(ExcludeJmodSectionPlugin.NAME, ExcludeJmodSectionPlugin.INCLUDE_HEADER_FILES); }, false, "--no-header-files"); mainOptions.add(plugOption); } } } private PluginOption getOption(String name) throws BadArgs { for (PluginOption o : pluginsOptions) { if (o.matches(name)) { return o; } } for (PluginOption o : mainOptions) { if (o.matches(name)) { return o; } } return null; } private PluginsConfiguration getPluginsConfig(Path output, Map launchers ) throws IOException, BadArgs { if (output != null) { if (Files.exists(output)) { throw new PluginException(PluginsResourceBundle. getMessage("err.dir.already.exits", output)); } } List pluginsList = new ArrayList<>(); for (Entry>> entry : pluginToMaps.entrySet()) { Plugin plugin = entry.getKey(); List> argsMaps = entry.getValue(); // same plugin option may be used multiple times in command line. // we call configure once for each occurrence. It is upto the plugin // to 'merge' and/or 'override' arguments. for (Map map : argsMaps) { try { plugin.configure(Collections.unmodifiableMap(map)); } catch (IllegalArgumentException e) { if (JlinkTask.DEBUG) { System.err.println("Plugin " + plugin.getName() + " threw exception with config: " + map); e.printStackTrace(); } throw e; } } if (!Utils.isDisabled(plugin)) { pluginsList.add(plugin); } } // recreate or postprocessing don't require an output directory. ImageBuilder builder = null; if (output != null) { builder = new DefaultImageBuilder(output, launchers); } return new Jlink.PluginsConfiguration(pluginsList, builder, lastSorter); } } private static final class ResourceBundleHelper { private final ResourceBundle bundle; private final ResourceBundle pluginBundle; ResourceBundleHelper(String path) { Locale locale = Locale.getDefault(); try { bundle = ResourceBundle.getBundle(path, locale); pluginBundle = ResourceBundle.getBundle("jdk.tools.jlink.resources.plugins", locale); } catch (MissingResourceException e) { throw new InternalError("Cannot find jlink resource bundle for locale " + locale); } } String getMessage(String key, Object... args) { String val; try { val = bundle.getString(key); } catch (MissingResourceException e) { // XXX OK, check in plugin bundle val = pluginBundle.getString(key); } return MessageFormat.format(val, args); } } public final class OptionsHelper { private final List> options; private String[] command; private String defaults; OptionsHelper(List> options) { this.options = options; } private boolean hasArgument(String optionName) throws BadArgs { Option opt = getOption(optionName); if (opt == null) { opt = pluginOptions.getOption(optionName); if (opt == null) { throw new BadArgs("err.unknown.option", optionName). showUsage(true); } } return opt.hasArg; } public boolean shouldListPlugins() { return pluginOptions.listPlugins; } private String getPluginsPath(String[] args) throws BadArgs { return null; } /** * Handles all options. This method stops processing the argument * at the first non-option argument i.e. not starts with `-`, or * at the first terminal option and returns the remaining arguments, * if any. */ public List handleOptions(T task, String[] args) throws BadArgs { // findbugs warning, copy instead of keeping a reference. command = Arrays.copyOf(args, args.length); // Must extract it prior to do any option analysis. // Required to interpret custom plugin options. // Unit tests can call Task multiple time in same JVM. pluginOptions = new PluginsHelper(null); // process options for (int i = 0; i < args.length; i++) { if (args[i].startsWith("-")) { String name = args[i]; PluginOption pluginOption = null; Option option = getOption(name); if (option == null) { pluginOption = pluginOptions.getOption(name); if (pluginOption == null) { throw new BadArgs("err.unknown.option", name). showUsage(true); } } Option opt = pluginOption == null ? option : pluginOption; String param = null; if (opt.hasArg) { if (name.startsWith("--") && name.indexOf('=') > 0) { param = name.substring(name.indexOf('=') + 1, name.length()); } else if (i + 1 < args.length) { param = args[++i]; } if (param == null || param.isEmpty() || (param.length() >= 2 && param.charAt(0) == '-' && param.charAt(1) == '-')) { throw new BadArgs("err.missing.arg", name). showUsage(true); } } if (pluginOption != null) { pluginOption.process(pluginOptions, name, param); } else { option.process(task, name, param); if (option.isTerminal()) { return ++i < args.length ? Stream.of(Arrays.copyOfRange(args, i, args.length)) .collect(Collectors.toList()) : Collections.emptyList(); } } if (opt.ignoreRest()) { i = args.length; } } else { return Stream.of(Arrays.copyOfRange(args, i, args.length)) .collect(Collectors.toList()); } } return Collections.emptyList(); } private Option getOption(String name) { for (Option o : options) { if (o.matches(name)) { return o; } } return null; } public void showHelp(String progName) { log.println(bundleHelper.getMessage("main.usage", progName)); Stream.concat(options.stream(), pluginOptions.mainOptions.stream()) .filter(option -> !option.isHidden()) .sorted() .forEach(option -> { log.println(bundleHelper.getMessage(option.resourceName())); }); log.println(bundleHelper.getMessage("main.command.files")); } public void listPlugins() { log.println("\n" + bundleHelper.getMessage("main.extended.help")); List pluginList = PluginRepository. getPlugins(pluginOptions.pluginsLayer); for (Plugin plugin : Utils.getSortedPlugins(pluginList)) { showPlugin(plugin, log); } log.println("\n" + bundleHelper.getMessage("main.extended.help.footer")); } private void showPlugin(Plugin plugin, PrintWriter log) { if (showsPlugin(plugin)) { log.println("\n" + bundleHelper.getMessage("main.plugin.name") + ": " + plugin.getName()); // print verbose details for non-builtin plugins if (!Utils.isBuiltin(plugin)) { log.println(bundleHelper.getMessage("main.plugin.class") + ": " + plugin.getClass().getName()); log.println(bundleHelper.getMessage("main.plugin.module") + ": " + plugin.getClass().getModule().getName()); Category category = plugin.getType(); log.println(bundleHelper.getMessage("main.plugin.category") + ": " + category.getName()); log.println(bundleHelper.getMessage("main.plugin.state") + ": " + plugin.getStateDescription()); } String option = plugin.getOption(); if (option != null) { log.println(bundleHelper.getMessage("main.plugin.option") + ": --" + plugin.getOption() + (plugin.hasArguments()? ("=" + plugin.getArgumentsDescription()) : "")); } // description can be long spanning more than one line and so // print a newline after description label. log.println(bundleHelper.getMessage("main.plugin.description") + ": " + plugin.getDescription()); } } String[] getInputCommand() { return command; } String getDefaults() { return defaults; } public ModuleLayer getPluginsLayer() { return pluginOptions.pluginsLayer; } } private PluginsHelper pluginOptions; private PrintWriter log; private final ResourceBundleHelper bundleHelper; public TaskHelper(String path) { if (!JLINK_BUNDLE.equals(path) && !JIMAGE_BUNDLE.equals(path)) { throw new IllegalArgumentException("Invalid Bundle"); } this.bundleHelper = new ResourceBundleHelper(path); } public OptionsHelper newOptionsHelper(Class clazz, Option[] options) { List> optionsList = new ArrayList<>(); for (Option o : options) { @SuppressWarnings("unchecked") Option opt = (Option) o; optionsList.add(opt); } return new OptionsHelper<>(optionsList); } public BadArgs newBadArgs(String key, Object... args) { return new BadArgs(key, args); } public String getMessage(String key, Object... args) { return bundleHelper.getMessage(key, args); } public void setLog(PrintWriter log) { this.log = log; } public void reportError(String key, Object... args) { log.println(bundleHelper.getMessage("error.prefix") + " " + bundleHelper.getMessage(key, args)); } public void reportUnknownError(String message) { log.println(bundleHelper.getMessage("error.prefix") + " " + message); } public void warning(String key, Object... args) { log.println(bundleHelper.getMessage("warn.prefix") + " " + bundleHelper.getMessage(key, args)); } public PluginsConfiguration getPluginsConfig(Path output, Map launchers) throws IOException, BadArgs { return pluginOptions.getPluginsConfig(output, launchers); } public Path getExistingImage() { return pluginOptions.existingImage; } public void showVersion(boolean full) { log.println(version(full ? "full" : "release")); } public String version(String key) { return System.getProperty("java.version"); } static ModuleLayer createPluginsLayer(List paths) { Path[] dirs = paths.toArray(new Path[0]); ModuleFinder finder = ModulePath.of(Runtime.version(), true, dirs); Configuration bootConfiguration = ModuleLayer.boot().configuration(); try { Configuration cf = bootConfiguration .resolveAndBind(ModuleFinder.of(), finder, Collections.emptySet()); ClassLoader scl = ClassLoader.getSystemClassLoader(); return ModuleLayer.boot().defineModulesWithOneLoader(cf, scl); } catch (Exception ex) { // Malformed plugin modules (e.g.: same package in multiple modules). throw new PluginException("Invalid modules in the plugins path: " + ex); } } // Display all plugins private static boolean showsPlugin(Plugin plugin) { return (!Utils.isDisabled(plugin) && plugin.getOption() != null); } }