1 /*
   2  * Copyright (c) 2015, 2017, 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.BufferedInputStream;
  28 import java.io.File;
  29 import java.io.IOException;
  30 import java.io.PrintWriter;
  31 import java.io.UncheckedIOException;
  32 import java.lang.module.Configuration;
  33 import java.lang.module.FindException;
  34 import java.lang.module.ModuleDescriptor;
  35 import java.lang.module.ModuleFinder;
  36 import java.lang.module.ModuleReference;
  37 import java.lang.module.ResolutionException;
  38 import java.lang.module.ResolvedModule;
  39 import java.net.URI;
  40 import java.nio.ByteOrder;
  41 import java.nio.file.Files;
  42 import java.nio.file.Path;
  43 import java.nio.file.Paths;
  44 import java.util.ArrayList;
  45 import java.util.Arrays;
  46 import java.util.Collection;
  47 import java.util.Collections;
  48 import java.util.Comparator;
  49 import java.util.Date;
  50 import java.util.HashMap;
  51 import java.util.HashSet;
  52 import java.util.List;
  53 import java.util.Locale;
  54 import java.util.Map;
  55 import java.util.Objects;
  56 import java.util.Optional;
  57 import java.util.Set;
  58 import java.util.stream.Collectors;
  59 import java.util.stream.Stream;
  60 
  61 import jdk.tools.jlink.internal.TaskHelper.BadArgs;
  62 import static jdk.tools.jlink.internal.TaskHelper.JLINK_BUNDLE;
  63 import jdk.tools.jlink.internal.Jlink.JlinkConfiguration;
  64 import jdk.tools.jlink.internal.Jlink.PluginsConfiguration;
  65 import jdk.tools.jlink.internal.TaskHelper.Option;
  66 import jdk.tools.jlink.internal.TaskHelper.OptionsHelper;
  67 import jdk.tools.jlink.internal.ImagePluginStack.ImageProvider;
  68 import jdk.tools.jlink.plugin.PluginException;
  69 import jdk.tools.jlink.builder.DefaultImageBuilder;
  70 import jdk.tools.jlink.plugin.Plugin;
  71 import jdk.internal.module.ModulePath;
  72 import jdk.internal.module.ModuleResolution;
  73 
  74 /**
  75  * Implementation for the jlink tool.
  76  *
  77  * ## Should use jdk.joptsimple some day.
  78  */
  79 public class JlinkTask {
  80     static final boolean DEBUG = Boolean.getBoolean("jlink.debug");
  81 
  82     // jlink API ignores by default. Remove when signing is implemented.
  83     static final boolean IGNORE_SIGNING_DEFAULT = true;
  84 
  85     private static final TaskHelper taskHelper
  86             = new TaskHelper(JLINK_BUNDLE);
  87 
  88     private static final Option<?>[] recognizedOptions = {
  89         new Option<JlinkTask>(false, (task, opt, arg) -> {
  90             task.options.help = true;
  91         }, "--help", "-h"),
  92         new Option<JlinkTask>(true, (task, opt, arg) -> {
  93             // if used multiple times, the last one wins!
  94             // So, clear previous values, if any.
  95             task.options.modulePath.clear();
  96             String[] dirs = arg.split(File.pathSeparator);
  97             int i = 0;
  98             Arrays.stream(dirs)
  99                   .map(Paths::get)
 100                   .forEach(task.options.modulePath::add);
 101         }, "--module-path", "-p"),
 102         new Option<JlinkTask>(true, (task, opt, arg) -> {
 103             // if used multiple times, the last one wins!
 104             // So, clear previous values, if any.
 105             task.options.limitMods.clear();
 106             for (String mn : arg.split(",")) {
 107                 if (mn.isEmpty()) {
 108                     throw taskHelper.newBadArgs("err.mods.must.be.specified",
 109                             "--limit-modules");
 110                 }
 111                 task.options.limitMods.add(mn);
 112             }
 113         }, "--limit-modules"),
 114         new Option<JlinkTask>(true, (task, opt, arg) -> {
 115             for (String mn : arg.split(",")) {
 116                 if (mn.isEmpty()) {
 117                     throw taskHelper.newBadArgs("err.mods.must.be.specified",
 118                             "--add-modules");
 119                 }
 120                 task.options.addMods.add(mn);
 121             }
 122         }, "--add-modules"),
 123         new Option<JlinkTask>(true, (task, opt, arg) -> {
 124             Path path = Paths.get(arg);
 125             task.options.output = path;
 126         }, "--output"),
 127         new Option<JlinkTask>(false, (task, opt, arg) -> {
 128             task.options.bindServices = true;
 129         }, "--bind-services"),
 130         new Option<JlinkTask>(false, (task, opt, arg) -> {
 131             task.options.suggestProviders = true;
 132         }, "--suggest-providers", "", true),
 133         new Option<JlinkTask>(true, (task, opt, arg) -> {
 134             String[] values = arg.split("=");
 135             // check values
 136             if (values.length != 2 || values[0].isEmpty() || values[1].isEmpty()) {
 137                 throw taskHelper.newBadArgs("err.launcher.value.format", arg);
 138             } else {
 139                 String commandName = values[0];
 140                 String moduleAndMain = values[1];
 141                 int idx = moduleAndMain.indexOf("/");
 142                 if (idx != -1) {
 143                     if (moduleAndMain.substring(0, idx).isEmpty()) {
 144                         throw taskHelper.newBadArgs("err.launcher.module.name.empty", arg);
 145                     }
 146 
 147                     if (moduleAndMain.substring(idx + 1).isEmpty()) {
 148                         throw taskHelper.newBadArgs("err.launcher.main.class.empty", arg);
 149                     }
 150                 }
 151                 task.options.launchers.put(commandName, moduleAndMain);
 152             }
 153         }, "--launcher"),
 154         new Option<JlinkTask>(true, (task, opt, arg) -> {
 155             if ("little".equals(arg)) {
 156                 task.options.endian = ByteOrder.LITTLE_ENDIAN;
 157             } else if ("big".equals(arg)) {
 158                 task.options.endian = ByteOrder.BIG_ENDIAN;
 159             } else {
 160                 throw taskHelper.newBadArgs("err.unknown.byte.order", arg);
 161             }
 162         }, "--endian"),
 163         new Option<JlinkTask>(false, (task, opt, arg) -> {
 164             task.options.verbose = true;
 165         }, "--verbose", "-v"),
 166         new Option<JlinkTask>(false, (task, opt, arg) -> {
 167             task.options.version = true;
 168         }, "--version"),
 169         new Option<JlinkTask>(true, (task, opt, arg) -> {
 170             Path path = Paths.get(arg);
 171             if (Files.exists(path)) {
 172                 throw taskHelper.newBadArgs("err.dir.exists", path);
 173             }
 174             task.options.packagedModulesPath = path;
 175         }, true, "--keep-packaged-modules"),
 176         new Option<JlinkTask>(true, (task, opt, arg) -> {
 177             task.options.saveoptsfile = arg;
 178         }, "--save-opts"),
 179         new Option<JlinkTask>(false, (task, opt, arg) -> {
 180             task.options.fullVersion = true;
 181         }, true, "--full-version"),
 182         new Option<JlinkTask>(false, (task, opt, arg) -> {
 183             task.options.ignoreSigning = true;
 184         }, "--ignore-signing-information"),};
 185 
 186     private static final String PROGNAME = "jlink";
 187     private final OptionsValues options = new OptionsValues();
 188 
 189     private static final OptionsHelper<JlinkTask> optionsHelper
 190             = taskHelper.newOptionsHelper(JlinkTask.class, recognizedOptions);
 191     private PrintWriter log;
 192 
 193     void setLog(PrintWriter out, PrintWriter err) {
 194         log = out;
 195         taskHelper.setLog(log);
 196     }
 197 
 198     /**
 199      * Result codes.
 200      */
 201     static final int
 202             EXIT_OK = 0, // Completed with no errors.
 203             EXIT_ERROR = 1, // Completed but reported errors.
 204             EXIT_CMDERR = 2, // Bad command-line arguments
 205             EXIT_SYSERR = 3, // System error or resource exhaustion.
 206             EXIT_ABNORMAL = 4;// terminated abnormally
 207 
 208     static class OptionsValues {
 209         boolean help;
 210         String  saveoptsfile;
 211         boolean verbose;
 212         boolean version;
 213         boolean fullVersion;
 214         final List<Path> modulePath = new ArrayList<>();
 215         final Set<String> limitMods = new HashSet<>();
 216         final Set<String> addMods = new HashSet<>();
 217         Path output;
 218         final Map<String, String> launchers = new HashMap<>();
 219         Path packagedModulesPath;
 220         ByteOrder endian = ByteOrder.nativeOrder();
 221         boolean ignoreSigning = false;
 222         boolean bindServices = false;
 223         boolean suggestProviders = false;
 224     }
 225 
 226     int run(String[] args) {
 227         if (log == null) {
 228             setLog(new PrintWriter(System.out, true),
 229                    new PrintWriter(System.err, true));
 230         }
 231         try {
 232             List<String> remaining = optionsHelper.handleOptions(this, args);
 233             if (remaining.size() > 0 && !options.suggestProviders) {
 234                 throw taskHelper.newBadArgs("err.orphan.arguments", toString(remaining))
 235                                 .showUsage(true);
 236             }
 237             if (options.help) {
 238                 optionsHelper.showHelp(PROGNAME);
 239                 return EXIT_OK;
 240             }
 241             if (optionsHelper.shouldListPlugins()) {
 242                 optionsHelper.listPlugins();
 243                 return EXIT_OK;
 244             }
 245             if (options.version || options.fullVersion) {
 246                 taskHelper.showVersion(options.fullVersion);
 247                 return EXIT_OK;
 248             }
 249 
 250             if (taskHelper.getExistingImage() != null) {
 251                 postProcessOnly(taskHelper.getExistingImage());
 252                 return EXIT_OK;
 253             }
 254 
 255 
 256             if (options.modulePath.isEmpty()) {
 257                 // no --module-path specified - try to set $JAVA_HOME/jmods if that exists
 258                 Path jmods = getDefaultModulePath();
 259                 if (jmods != null) {
 260                     options.modulePath.add(jmods);
 261                 }
 262 
 263                 if (options.modulePath.isEmpty()) {
 264                      throw taskHelper.newBadArgs("err.modulepath.must.be.specified")
 265                                  .showUsage(true);
 266                 }
 267             }
 268 
 269             JlinkConfiguration config = initJlinkConfig();
 270             if (options.suggestProviders) {
 271                 suggestProviders(config, remaining);
 272             } else {
 273                 createImage(config);
 274                 if (options.saveoptsfile != null) {
 275                     Files.write(Paths.get(options.saveoptsfile), getSaveOpts().getBytes());
 276                 }
 277             }
 278 
 279             return EXIT_OK;
 280         } catch (PluginException | IllegalArgumentException |
 281                  UncheckedIOException |IOException | FindException | ResolutionException e) {
 282             log.println(taskHelper.getMessage("error.prefix") + " " + e.getMessage());
 283             if (DEBUG) {
 284                 e.printStackTrace(log);
 285             }
 286             return EXIT_ERROR;
 287         } catch (BadArgs e) {
 288             taskHelper.reportError(e.key, e.args);
 289             if (e.showUsage) {
 290                 log.println(taskHelper.getMessage("main.usage.summary", PROGNAME));
 291             }
 292             if (DEBUG) {
 293                 e.printStackTrace(log);
 294             }
 295             return EXIT_CMDERR;
 296         } catch (Throwable x) {
 297             log.println(taskHelper.getMessage("error.prefix") + " " + x.getMessage());
 298             x.printStackTrace(log);
 299             return EXIT_ABNORMAL;
 300         } finally {
 301             log.flush();
 302         }
 303     }
 304 
 305     /*
 306      * Jlink API entry point.
 307      */
 308     public static void createImage(JlinkConfiguration config,
 309                                    PluginsConfiguration plugins)
 310             throws Exception {
 311         Objects.requireNonNull(config);
 312         Objects.requireNonNull(config.getOutput());
 313         plugins = plugins == null ? new PluginsConfiguration() : plugins;
 314 
 315         // First create the image provider
 316         ImageProvider imageProvider =
 317                 createImageProvider(config,
 318                                     null,
 319                                     IGNORE_SIGNING_DEFAULT,
 320                                     false,
 321                                     false,
 322                                     null);
 323 
 324         // Then create the Plugin Stack
 325         ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(plugins);
 326 
 327         //Ask the stack to proceed;
 328         stack.operate(imageProvider);
 329     }
 330 
 331     /*
 332      * Jlink API entry point.
 333      */
 334     public static void postProcessImage(ExecutableImage image, List<Plugin> postProcessorPlugins)
 335             throws Exception {
 336         Objects.requireNonNull(image);
 337         Objects.requireNonNull(postProcessorPlugins);
 338         PluginsConfiguration config = new PluginsConfiguration(postProcessorPlugins);
 339         ImagePluginStack stack = ImagePluginConfiguration.
 340                 parseConfiguration(config);
 341 
 342         stack.operate((ImagePluginStack stack1) -> image);
 343     }
 344 
 345     private void postProcessOnly(Path existingImage) throws Exception {
 346         PluginsConfiguration config = taskHelper.getPluginsConfig(null, null);
 347         ExecutableImage img = DefaultImageBuilder.getExecutableImage(existingImage);
 348         if (img == null) {
 349             throw taskHelper.newBadArgs("err.existing.image.invalid");
 350         }
 351         postProcessImage(img, config.getPlugins());
 352     }
 353 
 354     // the token for "all modules on the module path"
 355     private static final String ALL_MODULE_PATH = "ALL-MODULE-PATH";
 356     private JlinkConfiguration initJlinkConfig() throws BadArgs {
 357         Set<String> roots = new HashSet<>();
 358         for (String mod : options.addMods) {
 359             if (mod.equals(ALL_MODULE_PATH)) {
 360                 ModuleFinder finder = newModuleFinder(options.modulePath, options.limitMods, Set.of());
 361                 // all observable modules are roots
 362                 finder.findAll()
 363                       .stream()
 364                       .map(ModuleReference::descriptor)
 365                       .map(ModuleDescriptor::name)
 366                       .forEach(mn -> roots.add(mn));
 367             } else {
 368                 roots.add(mod);
 369             }
 370         }
 371 
 372         ModuleFinder finder = newModuleFinder(options.modulePath, options.limitMods, roots);
 373         if (!finder.find("java.base").isPresent()) {
 374             Path defModPath = getDefaultModulePath();
 375             if (defModPath != null) {
 376                 options.modulePath.add(defModPath);
 377             }
 378             finder = newModuleFinder(options.modulePath, options.limitMods, roots);
 379         }
 380 
 381         return new JlinkConfiguration(options.output,
 382                                       roots,
 383                                       options.endian,
 384                                       finder);
 385     }
 386 
 387     private void createImage(JlinkConfiguration config) throws Exception {
 388         if (options.output == null) {
 389             throw taskHelper.newBadArgs("err.output.must.be.specified").showUsage(true);
 390         }
 391         if (options.addMods.isEmpty()) {
 392             throw taskHelper.newBadArgs("err.mods.must.be.specified", "--add-modules")
 393                             .showUsage(true);
 394         }
 395 
 396         // First create the image provider
 397         ImageProvider imageProvider = createImageProvider(config,
 398                                                           options.packagedModulesPath,
 399                                                           options.ignoreSigning,
 400                                                           options.bindServices,
 401                                                           options.verbose,
 402                                                           log);
 403 
 404         // Then create the Plugin Stack
 405         ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(
 406             taskHelper.getPluginsConfig(options.output, options.launchers));
 407 
 408         //Ask the stack to proceed
 409         stack.operate(imageProvider);
 410     }
 411 
 412     /**
 413      * @return the system module path or null
 414      */
 415     public static Path getDefaultModulePath() {
 416         Path jmods = Paths.get(System.getProperty("java.home"), "jmods");
 417         return Files.isDirectory(jmods)? jmods : null;
 418     }
 419 
 420     /*
 421      * Returns a module finder of the given module path that limits
 422      * the observable modules to those in the transitive closure of
 423      * the modules specified in {@code limitMods} plus other modules
 424      * specified in the {@code roots} set.
 425      *
 426      * @throws IllegalArgumentException if java.base module is present
 427      * but its descriptor has no version
 428      */
 429     public static ModuleFinder newModuleFinder(List<Path> paths,
 430                                                Set<String> limitMods,
 431                                                Set<String> roots)
 432     {
 433         if (Objects.requireNonNull(paths).isEmpty()) {
 434              throw new IllegalArgumentException(taskHelper.getMessage("err.empty.module.path"));
 435         }
 436 
 437         Path[] entries = paths.toArray(new Path[0]);
 438         Runtime.Version version = Runtime.version();
 439         ModuleFinder finder = ModulePath.of(version, true, entries);
 440 
 441         if (finder.find("java.base").isPresent()) {
 442             // use the version of java.base module, if present, as
 443             // the release version for multi-release JAR files
 444             ModuleDescriptor.Version v = finder.find("java.base").get()
 445                 .descriptor().version().orElseThrow(() ->
 446                     new IllegalArgumentException("No version in java.base descriptor")
 447                 );
 448 
 449             // java.base version is different than the current runtime version
 450             version = Runtime.Version.parse(v.toString());
 451             if (Runtime.version().feature() != version.feature() ||
 452                 Runtime.version().interim() != version.interim())
 453             {
 454                 // jlink version and java.base version do not match.
 455                 // We do not (yet) support this mode.
 456                 throw new IllegalArgumentException(taskHelper.getMessage("err.jlink.version.mismatch",
 457                     Runtime.version().feature(), Runtime.version().interim(),
 458                     version.feature(), version.interim()));
 459             }
 460         }
 461 
 462         // if limitmods is specified then limit the universe
 463         if (limitMods != null && !limitMods.isEmpty()) {
 464             finder = limitFinder(finder, limitMods, Objects.requireNonNull(roots));
 465         }
 466         return finder;
 467     }
 468 
 469     private static Path toPathLocation(ResolvedModule m) {
 470         Optional<URI> ouri = m.reference().location();
 471         if (!ouri.isPresent())
 472             throw new InternalError(m + " does not have a location");
 473         URI uri = ouri.get();
 474         return Paths.get(uri);
 475     }
 476 
 477 
 478     private static ImageProvider createImageProvider(JlinkConfiguration config,
 479                                                      Path retainModulesPath,
 480                                                      boolean ignoreSigning,
 481                                                      boolean bindService,
 482                                                      boolean verbose,
 483                                                      PrintWriter log)
 484             throws IOException
 485     {
 486         Configuration cf = bindService ? config.resolveAndBind()
 487                                        : config.resolve();
 488 
 489         cf.modules().stream()
 490             .map(ResolvedModule::reference)
 491             .filter(mref -> mref.descriptor().isAutomatic())
 492             .findAny()
 493             .ifPresent(mref -> {
 494                 String loc = mref.location().map(URI::toString).orElse("<unknown>");
 495                 throw new IllegalArgumentException(
 496                     taskHelper.getMessage("err.automatic.module", mref.descriptor().name(), loc));
 497             });
 498 
 499         if (verbose && log != null) {
 500             // print modules to be linked in
 501             cf.modules().stream()
 502               .sorted(Comparator.comparing(ResolvedModule::name))
 503               .forEach(rm -> log.format("%s %s%n",
 504                                         rm.name(), rm.reference().location().get()));
 505 
 506             // print provider info
 507             Set<ModuleReference> references = cf.modules().stream()
 508                 .map(ResolvedModule::reference).collect(Collectors.toSet());
 509 
 510             String msg = String.format("%n%s:", taskHelper.getMessage("providers.header"));
 511             printProviders(log, msg, references);
 512         }
 513 
 514         // emit a warning for any incubating modules in the configuration
 515         if (log != null) {
 516             String im = cf.modules()
 517                           .stream()
 518                           .map(ResolvedModule::reference)
 519                           .filter(ModuleResolution::hasIncubatingWarning)
 520                           .map(ModuleReference::descriptor)
 521                           .map(ModuleDescriptor::name)
 522                           .collect(Collectors.joining(", "));
 523 
 524             if (!"".equals(im))
 525                 log.println("WARNING: Using incubator modules: " + im);
 526         }
 527 
 528         Map<String, Path> mods = cf.modules().stream()
 529             .collect(Collectors.toMap(ResolvedModule::name, JlinkTask::toPathLocation));
 530         return new ImageHelper(cf, mods, config.getByteOrder(), retainModulesPath, ignoreSigning);
 531     }
 532 
 533     /*
 534      * Returns a ModuleFinder that limits observability to the given root
 535      * modules, their transitive dependences, plus a set of other modules.
 536      */
 537     public static ModuleFinder limitFinder(ModuleFinder finder,
 538                                            Set<String> roots,
 539                                            Set<String> otherMods) {
 540 
 541         // resolve all root modules
 542         Configuration cf = Configuration.empty()
 543                 .resolve(finder,
 544                          ModuleFinder.of(),
 545                          roots);
 546 
 547         // module name -> reference
 548         Map<String, ModuleReference> map = new HashMap<>();
 549         cf.modules().forEach(m -> {
 550             ModuleReference mref = m.reference();
 551             map.put(mref.descriptor().name(), mref);
 552         });
 553 
 554         // add the other modules
 555         otherMods.stream()
 556             .map(finder::find)
 557             .flatMap(Optional::stream)
 558             .forEach(mref -> map.putIfAbsent(mref.descriptor().name(), mref));
 559 
 560         // set of modules that are observable
 561         Set<ModuleReference> mrefs = new HashSet<>(map.values());
 562 
 563         return new ModuleFinder() {
 564             @Override
 565             public Optional<ModuleReference> find(String name) {
 566                 return Optional.ofNullable(map.get(name));
 567             }
 568 
 569             @Override
 570             public Set<ModuleReference> findAll() {
 571                 return mrefs;
 572             }
 573         };
 574     }
 575 
 576     /*
 577      * Returns a map of each service type to the modules that use it
 578      * It will include services that are provided by a module but may not used
 579      * by any of the observable modules.
 580      */
 581     private static Map<String, Set<String>> uses(Set<ModuleReference> modules) {
 582         // collects the services used by the modules and print uses
 583         Map<String, Set<String>> services = new HashMap<>();
 584         modules.stream()
 585                .map(ModuleReference::descriptor)
 586                .forEach(md -> {
 587                    // include services that may not be used by any observable modules
 588                    md.provides().forEach(p ->
 589                        services.computeIfAbsent(p.service(), _k -> new HashSet<>()));
 590                    md.uses().forEach(s -> services.computeIfAbsent(s, _k -> new HashSet<>())
 591                                                   .add(md.name()));
 592                });
 593         return services;
 594     }
 595 
 596     private static void printProviders(PrintWriter log,
 597                                        String header,
 598                                        Set<ModuleReference> modules) {
 599         printProviders(log, header, modules, uses(modules));
 600     }
 601 
 602     /*
 603      * Prints the providers that are used by the specified services.
 604      *
 605      * The specified services maps a service type name to the modules
 606      * using the service type which may be empty if no observable module uses
 607      * that service.
 608      */
 609     private static void printProviders(PrintWriter log,
 610                                        String header,
 611                                        Set<ModuleReference> modules,
 612                                        Map<String, Set<String>> serviceToUses) {
 613         if (modules.isEmpty())
 614             return;
 615 
 616         // Build a map of a service type to the provider modules
 617         Map<String, Set<ModuleDescriptor>> providers = new HashMap<>();
 618         modules.stream()
 619             .map(ModuleReference::descriptor)
 620             .forEach(md -> {
 621                 md.provides().stream()
 622                   .filter(p -> serviceToUses.containsKey(p.service()))
 623                   .forEach(p -> providers.computeIfAbsent(p.service(), _k -> new HashSet<>())
 624                                          .add(md));
 625             });
 626 
 627         if (!providers.isEmpty()) {
 628             log.println(header);
 629         }
 630 
 631         // print the providers of the service types used by the specified modules
 632         // sorted by the service type name and then provider's module name
 633         providers.entrySet().stream()
 634             .sorted(Map.Entry.comparingByKey())
 635             .forEach(e -> {
 636                 String service = e.getKey();
 637                 e.getValue().stream()
 638                  .sorted(Comparator.comparing(ModuleDescriptor::name))
 639                  .forEach(md ->
 640                      md.provides().stream()
 641                        .filter(p -> p.service().equals(service))
 642                        .forEach(p -> {
 643                            String usedBy;
 644                            if (serviceToUses.get(p.service()).isEmpty()) {
 645                                usedBy = "not used by any observable module";
 646                            } else {
 647                                usedBy = serviceToUses.get(p.service()).stream()
 648                                             .sorted()
 649                                             .collect(Collectors.joining(",", "used by ", ""));
 650                            }
 651                            log.format("  %s provides %s %s%n",
 652                                       md.name(), p.service(), usedBy);
 653                        })
 654                  );
 655             });
 656     }
 657 
 658     private void suggestProviders(JlinkConfiguration config, List<String> args)
 659         throws BadArgs
 660     {
 661         if (args.size() > 1) {
 662             throw taskHelper.newBadArgs("err.orphan.argument",
 663                                         toString(args.subList(1, args.size())))
 664                             .showUsage(true);
 665         }
 666 
 667         if (options.bindServices) {
 668             log.println(taskHelper.getMessage("no.suggested.providers"));
 669             return;
 670         }
 671 
 672         ModuleFinder finder = config.finder();
 673         if (args.isEmpty()) {
 674             // print providers used by the observable modules without service binding
 675             Set<ModuleReference> mrefs = finder.findAll();
 676             // print uses of the modules that would be linked into the image
 677             mrefs.stream()
 678                  .sorted(Comparator.comparing(mref -> mref.descriptor().name()))
 679                  .forEach(mref -> {
 680                      ModuleDescriptor md = mref.descriptor();
 681                      log.format("%s %s%n", md.name(),
 682                                 mref.location().get());
 683                      md.uses().stream().sorted()
 684                        .forEach(s -> log.format("    uses %s%n", s));
 685                  });
 686 
 687             String msg = String.format("%n%s:", taskHelper.getMessage("suggested.providers.header"));
 688             printProviders(log, msg, mrefs, uses(mrefs));
 689 
 690         } else {
 691             // comma-separated service types, if specified
 692             Set<String> names = Stream.of(args.get(0).split(","))
 693                 .collect(Collectors.toSet());
 694             // find the modules that provide the specified service
 695             Set<ModuleReference> mrefs = finder.findAll().stream()
 696                 .filter(mref -> mref.descriptor().provides().stream()
 697                                     .map(ModuleDescriptor.Provides::service)
 698                                     .anyMatch(names::contains))
 699                 .collect(Collectors.toSet());
 700 
 701             // find the modules that uses the specified services
 702             Map<String, Set<String>> uses = new HashMap<>();
 703             names.forEach(s -> uses.computeIfAbsent(s, _k -> new HashSet<>()));
 704             finder.findAll().stream()
 705                   .map(ModuleReference::descriptor)
 706                   .forEach(md -> md.uses().stream()
 707                                    .filter(names::contains)
 708                                    .forEach(s -> uses.get(s).add(md.name())));
 709 
 710             // check if any name given on the command line are not provided by any module
 711             mrefs.stream()
 712                  .flatMap(mref -> mref.descriptor().provides().stream()
 713                                       .map(ModuleDescriptor.Provides::service))
 714                  .forEach(names::remove);
 715             if (!names.isEmpty()) {
 716                 log.println(taskHelper.getMessage("warn.provider.notfound",
 717                                                   toString(names)));
 718             }
 719 
 720             String msg = String.format("%n%s:", taskHelper.getMessage("suggested.providers.header"));
 721             printProviders(log, msg, mrefs, uses);
 722         }
 723     }
 724 
 725     private static String toString(Collection<String> collection) {
 726         return collection.stream().sorted()
 727                          .collect(Collectors.joining(","));
 728     }
 729 
 730     private String getSaveOpts() {
 731         StringBuilder sb = new StringBuilder();
 732         sb.append('#').append(new Date()).append("\n");
 733         for (String c : optionsHelper.getInputCommand()) {
 734             sb.append(c).append(" ");
 735         }
 736 
 737         return sb.toString();
 738     }
 739 
 740     private static String getBomHeader() {
 741         StringBuilder sb = new StringBuilder();
 742         sb.append("#").append(new Date()).append("\n");
 743         sb.append("#Please DO NOT Modify this file").append("\n");
 744         return sb.toString();
 745     }
 746 
 747     private String genBOMContent() throws IOException {
 748         StringBuilder sb = new StringBuilder();
 749         sb.append(getBomHeader());
 750         StringBuilder command = new StringBuilder();
 751         for (String c : optionsHelper.getInputCommand()) {
 752             command.append(c).append(" ");
 753         }
 754         sb.append("command").append(" = ").append(command);
 755         sb.append("\n");
 756 
 757         return sb.toString();
 758     }
 759 
 760     private static String genBOMContent(JlinkConfiguration config,
 761             PluginsConfiguration plugins)
 762             throws IOException {
 763         StringBuilder sb = new StringBuilder();
 764         sb.append(getBomHeader());
 765         sb.append(config);
 766         sb.append(plugins);
 767         return sb.toString();
 768     }
 769 
 770     private static class ImageHelper implements ImageProvider {
 771         final ByteOrder order;
 772         final Path packagedModulesPath;
 773         final boolean ignoreSigning;
 774         final Runtime.Version version;
 775         final Set<Archive> archives;
 776 
 777         ImageHelper(Configuration cf,
 778                     Map<String, Path> modsPaths,
 779                     ByteOrder order,
 780                     Path packagedModulesPath,
 781                     boolean ignoreSigning) throws IOException {
 782             this.order = order;
 783             this.packagedModulesPath = packagedModulesPath;
 784             this.ignoreSigning = ignoreSigning;
 785 
 786             // use the version of java.base module, if present, as
 787             // the release version for multi-release JAR files
 788             this.version = cf.findModule("java.base")
 789                 .map(ResolvedModule::reference)
 790                 .map(ModuleReference::descriptor)
 791                 .flatMap(ModuleDescriptor::version)
 792                 .map(ModuleDescriptor.Version::toString)
 793                 .map(Runtime.Version::parse)
 794                 .orElse(Runtime.version());
 795 
 796             this.archives = modsPaths.entrySet().stream()
 797                                 .map(e -> newArchive(e.getKey(), e.getValue()))
 798                                 .collect(Collectors.toSet());
 799         }
 800 
 801         private Archive newArchive(String module, Path path) {
 802             if (path.toString().endsWith(".jmod")) {
 803                 return new JmodArchive(module, path);
 804             } else if (path.toString().endsWith(".jar")) {
 805                 ModularJarArchive modularJarArchive = new ModularJarArchive(module, path, version);
 806 
 807                 Stream<Archive.Entry> signatures = modularJarArchive.entries().filter((entry) -> {
 808                     String name = entry.name().toUpperCase(Locale.ENGLISH);
 809 
 810                     return name.startsWith("META-INF/") && name.indexOf('/', 9) == -1 && (
 811                                 name.endsWith(".SF") ||
 812                                 name.endsWith(".DSA") ||
 813                                 name.endsWith(".RSA") ||
 814                                 name.endsWith(".EC") ||
 815                                 name.startsWith("META-INF/SIG-")
 816                             );
 817                 });
 818 
 819                 if (signatures.count() != 0) {
 820                     if (ignoreSigning) {
 821                         System.err.println(taskHelper.getMessage("warn.signing", path));
 822                     } else {
 823                         throw new IllegalArgumentException(taskHelper.getMessage("err.signing", path));
 824                     }
 825                 }
 826 
 827                 return modularJarArchive;
 828             } else if (Files.isDirectory(path)) {
 829                 Path modInfoPath = path.resolve("module-info.class");
 830                 if (Files.isRegularFile(modInfoPath)) {
 831                     return new DirArchive(path, findModuleName(modInfoPath));
 832                 } else {
 833                     throw new IllegalArgumentException(
 834                         taskHelper.getMessage("err.not.a.module.directory", path));
 835                 }
 836             } else {
 837                 throw new IllegalArgumentException(
 838                     taskHelper.getMessage("err.not.modular.format", module, path));
 839             }
 840         }
 841 
 842         private static String findModuleName(Path modInfoPath) {
 843             try (BufferedInputStream bis = new BufferedInputStream(
 844                     Files.newInputStream(modInfoPath))) {
 845                 return ModuleDescriptor.read(bis).name();
 846             } catch (IOException exp) {
 847                 throw new IllegalArgumentException(taskHelper.getMessage(
 848                     "err.cannot.read.module.info", modInfoPath), exp);
 849             }
 850         }
 851 
 852         @Override
 853         public ExecutableImage retrieve(ImagePluginStack stack) throws IOException {
 854             ExecutableImage image = ImageFileCreator.create(archives, order, stack);
 855             if (packagedModulesPath != null) {
 856                 // copy the packaged modules to the given path
 857                 Files.createDirectories(packagedModulesPath);
 858                 for (Archive a : archives) {
 859                     Path file = a.getPath();
 860                     Path dest = packagedModulesPath.resolve(file.getFileName());
 861                     Files.copy(file, dest);
 862                 }
 863             }
 864             return image;
 865         }
 866     }
 867 }