1 /*
   2  * Copyright (c) 2012, 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 
  26 package com.sun.tools.jdeps;
  27 
  28 import static com.sun.tools.jdeps.Module.trace;
  29 import static java.util.stream.Collectors.*;
  30 
  31 import com.sun.tools.classfile.Dependency;
  32 
  33 import java.io.BufferedInputStream;
  34 import java.io.File;
  35 import java.io.FileNotFoundException;
  36 import java.io.IOException;
  37 import java.io.InputStream;
  38 import java.io.UncheckedIOException;
  39 import java.lang.module.Configuration;
  40 import java.lang.module.ModuleDescriptor;
  41 import java.lang.module.ModuleDescriptor.Exports;
  42 import java.lang.module.ModuleDescriptor.Opens;
  43 import java.lang.module.ModuleFinder;
  44 import java.lang.module.ModuleReader;
  45 import java.lang.module.ModuleReference;
  46 import java.lang.module.ResolvedModule;
  47 import java.net.URI;
  48 import java.nio.file.DirectoryStream;
  49 import java.nio.file.FileSystem;
  50 import java.nio.file.FileSystems;
  51 import java.nio.file.Files;
  52 import java.nio.file.Path;
  53 import java.nio.file.Paths;
  54 import java.util.ArrayList;
  55 import java.util.Collections;
  56 import java.util.HashMap;
  57 import java.util.HashSet;
  58 import java.util.LinkedHashMap;
  59 import java.util.LinkedHashSet;
  60 import java.util.List;
  61 import java.util.Map;
  62 import java.util.Objects;
  63 import java.util.Optional;
  64 import java.util.Set;
  65 import java.util.function.Function;
  66 import java.util.function.Supplier;
  67 import java.util.stream.Stream;
  68 
  69 public class JdepsConfiguration implements AutoCloseable {
  70     // the token for "all modules on the module path"
  71     public static final String ALL_MODULE_PATH = "ALL-MODULE-PATH";
  72     public static final String ALL_DEFAULT = "ALL-DEFAULT";
  73     public static final String ALL_SYSTEM = "ALL-SYSTEM";
  74     public static final String MODULE_INFO = "module-info.class";
  75 
  76     private final SystemModuleFinder system;
  77     private final ModuleFinder finder;
  78 
  79     private final Map<String, Module> nameToModule = new LinkedHashMap<>();
  80     private final Map<String, Module> packageToModule = new HashMap<>();
  81     private final Map<String, List<Archive>> packageToUnnamedModule = new HashMap<>();
  82 
  83     private final List<Archive> classpathArchives = new ArrayList<>();
  84     private final List<Archive> initialArchives = new ArrayList<>();
  85     private final Set<Module> rootModules = new HashSet<>();
  86     private final Configuration configuration;
  87     private final Runtime.Version version;
  88 
  89     private JdepsConfiguration(SystemModuleFinder systemModulePath,
  90                                ModuleFinder finder,
  91                                Set<String> roots,
  92                                List<Path> classpaths,
  93                                List<Archive> initialArchives,
  94                                boolean allDefaultModules,
  95                                boolean allSystemModules,
  96                                Runtime.Version version)
  97         throws IOException
  98     {
  99         trace("root: %s%n", roots);
 100 
 101         this.system = systemModulePath;
 102         this.finder = finder;
 103         this.version = version;
 104 
 105         // build root set for resolution
 106         Set<String> mods = new HashSet<>(roots);
 107 
 108         // add all system modules to the root set for unnamed module or set explicitly
 109         boolean unnamed = !initialArchives.isEmpty() || !classpaths.isEmpty();
 110         if (allSystemModules || (unnamed && !allDefaultModules)) {
 111             systemModulePath.findAll().stream()
 112                 .map(mref -> mref.descriptor().name())
 113                 .forEach(mods::add);
 114         }
 115 
 116         if (allDefaultModules) {
 117             mods.addAll(systemModulePath.defaultSystemRoots());
 118         }
 119 
 120         this.configuration = Configuration.empty()
 121                 .resolve(finder, ModuleFinder.of(), mods);
 122 
 123         this.configuration.modules().stream()
 124                 .map(ResolvedModule::reference)
 125                 .forEach(this::addModuleReference);
 126 
 127         // packages in unnamed module
 128         initialArchives.forEach(archive -> {
 129             addPackagesInUnnamedModule(archive);
 130             this.initialArchives.add(archive);
 131         });
 132 
 133         // classpath archives
 134         for (Path p : classpaths) {
 135             if (Files.exists(p)) {
 136                 Archive archive = Archive.getInstance(p, version);
 137                 addPackagesInUnnamedModule(archive);
 138                 classpathArchives.add(archive);
 139             }
 140         }
 141 
 142         // all roots specified in --add-modules or -m are included
 143         // as the initial set for analysis.
 144         roots.stream()
 145              .map(nameToModule::get)
 146              .forEach(this.rootModules::add);
 147 
 148         initProfiles();
 149 
 150         trace("resolved modules: %s%n", nameToModule.keySet().stream()
 151                 .sorted().collect(joining("\n", "\n", "")));
 152     }
 153 
 154     private void initProfiles() {
 155         // other system modules are not observed and not added in nameToModule map
 156         Map<String, Module> systemModules =
 157             system.moduleNames()
 158                 .collect(toMap(Function.identity(), (mn) -> {
 159                     Module m = nameToModule.get(mn);
 160                     if (m == null) {
 161                         ModuleReference mref = finder.find(mn).get();
 162                         m = toModule(mref);
 163                     }
 164                     return m;
 165                 }));
 166         Profile.init(systemModules);
 167     }
 168 
 169     private void addModuleReference(ModuleReference mref) {
 170         Module module = toModule(mref);
 171         nameToModule.put(mref.descriptor().name(), module);
 172         mref.descriptor().packages()
 173             .forEach(pn -> packageToModule.putIfAbsent(pn, module));
 174     }
 175 
 176     private void addPackagesInUnnamedModule(Archive archive) {
 177         archive.reader().entries().stream()
 178                .filter(e -> e.endsWith(".class") && !e.equals(MODULE_INFO))
 179                .map(this::toPackageName)
 180                .distinct()
 181                .forEach(pn -> packageToUnnamedModule
 182                    .computeIfAbsent(pn, _n -> new ArrayList<>()).add(archive));
 183     }
 184 
 185     private String toPackageName(String name) {
 186         int i = name.lastIndexOf('/');
 187         return i > 0 ? name.replace('/', '.').substring(0, i) : "";
 188     }
 189 
 190     public Optional<Module> findModule(String name) {
 191         Objects.requireNonNull(name);
 192         Module m = nameToModule.get(name);
 193         return m!= null ? Optional.of(m) : Optional.empty();
 194 
 195     }
 196 
 197     public Optional<ModuleDescriptor> findModuleDescriptor(String name) {
 198         Objects.requireNonNull(name);
 199         Module m = nameToModule.get(name);
 200         return m!= null ? Optional.of(m.descriptor()) : Optional.empty();
 201     }
 202 
 203     boolean isValidToken(String name) {
 204         return ALL_MODULE_PATH.equals(name) ||
 205                 ALL_DEFAULT.equals(name) ||
 206                 ALL_SYSTEM.equals(name);
 207     }
 208 
 209     /**
 210      * Returns the modules that the given module can read
 211      */
 212     public Stream<Module> reads(Module module) {
 213         return configuration.findModule(module.name()).get()
 214             .reads().stream()
 215             .map(ResolvedModule::name)
 216             .map(nameToModule::get);
 217     }
 218 
 219     /**
 220      * Returns the list of packages that split between resolved module and
 221      * unnamed module
 222      */
 223     public Map<String, Set<String>> splitPackages() {
 224         Set<String> splitPkgs = packageToModule.keySet().stream()
 225                                        .filter(packageToUnnamedModule::containsKey)
 226                                        .collect(toSet());
 227         if (splitPkgs.isEmpty())
 228             return Collections.emptyMap();
 229 
 230         return splitPkgs.stream().collect(toMap(Function.identity(), (pn) -> {
 231             Set<String> sources = new LinkedHashSet<>();
 232             sources.add(packageToModule.get(pn).getModule().location().toString());
 233             packageToUnnamedModule.get(pn).stream()
 234                 .map(Archive::getPathName)
 235                 .forEach(sources::add);
 236             return sources;
 237         }));
 238     }
 239 
 240     /**
 241      * Returns an optional archive containing the given Location
 242      */
 243     public Optional<Archive> findClass(Dependency.Location location) {
 244         String name = location.getName();
 245         int i = name.lastIndexOf('/');
 246         String pn = i > 0 ? name.substring(0, i).replace('/', '.') : "";
 247         Archive archive = packageToModule.get(pn);
 248         if (archive != null) {
 249             return archive.contains(name + ".class")
 250                         ? Optional.of(archive)
 251                         : Optional.empty();
 252         }
 253 
 254         if (packageToUnnamedModule.containsKey(pn)) {
 255             return packageToUnnamedModule.get(pn).stream()
 256                     .filter(a -> a.contains(name + ".class"))
 257                     .findFirst();
 258         }
 259         return Optional.empty();
 260     }
 261 
 262     /**
 263      * Returns the list of Modules that can be found in the specified
 264      * module paths.
 265      */
 266     public Map<String, Module> getModules() {
 267         return nameToModule;
 268     }
 269 
 270     public Stream<Module> resolve(Set<String> roots) {
 271         if (roots.isEmpty()) {
 272             return nameToModule.values().stream();
 273         } else {
 274             return Configuration.empty()
 275                     .resolve(finder, ModuleFinder.of(), roots)
 276                     .modules().stream()
 277                     .map(ResolvedModule::name)
 278                     .map(nameToModule::get);
 279         }
 280     }
 281 
 282     public List<Archive> classPathArchives() {
 283         return classpathArchives;
 284     }
 285 
 286     public List<Archive> initialArchives() {
 287         return initialArchives;
 288     }
 289 
 290     public Set<Module> rootModules() {
 291         return rootModules;
 292     }
 293 
 294     public Module toModule(ModuleReference mref) {
 295         try {
 296             String mn = mref.descriptor().name();
 297             URI location = mref.location().orElseThrow(FileNotFoundException::new);
 298             ModuleDescriptor md = mref.descriptor();
 299             Module.Builder builder = new Module.Builder(md, system.find(mn).isPresent());
 300 
 301             final ClassFileReader reader;
 302             if (location.getScheme().equals("jrt")) {
 303                 reader = system.getClassReader(mn);
 304             } else {
 305                 reader = ClassFileReader.newInstance(Paths.get(location), version);
 306             }
 307 
 308             builder.classes(reader);
 309             builder.location(location);
 310 
 311             return builder.build();
 312         } catch (IOException e) {
 313             throw new UncheckedIOException(e);
 314         }
 315     }
 316 
 317     public Runtime.Version getVersion() {
 318         return version;
 319     }
 320 
 321     /*
 322      * Close all archives e.g. JarFile
 323      */
 324     @Override
 325     public void close() throws IOException {
 326         for (Archive archive : initialArchives)
 327             archive.close();
 328         for (Archive archive : classpathArchives)
 329             archive.close();
 330         for (Module module : nameToModule.values())
 331             module.close();
 332     }
 333 
 334     static class SystemModuleFinder implements ModuleFinder {
 335         private static final String JAVA_HOME = System.getProperty("java.home");
 336         private static final String JAVA_SE = "java.se";
 337 
 338         private final FileSystem fileSystem;
 339         private final Path root;
 340         private final Map<String, ModuleReference> systemModules;
 341 
 342         SystemModuleFinder() {
 343             if (Files.isRegularFile(Paths.get(JAVA_HOME, "lib", "modules"))) {
 344                 // jrt file system
 345                 this.fileSystem = FileSystems.getFileSystem(URI.create("jrt:/"));
 346                 this.root = fileSystem.getPath("/modules");
 347                 this.systemModules = walk(root);
 348             } else {
 349                 // exploded image
 350                 this.fileSystem = FileSystems.getDefault();
 351                 root = Paths.get(JAVA_HOME, "modules");
 352                 this.systemModules = ModuleFinder.ofSystem().findAll().stream()
 353                     .collect(toMap(mref -> mref.descriptor().name(), Function.identity()));
 354             }
 355         }
 356 
 357         SystemModuleFinder(String javaHome) throws IOException {
 358             if (javaHome == null) {
 359                 // --system none
 360                 this.fileSystem = null;
 361                 this.root = null;
 362                 this.systemModules = Collections.emptyMap();
 363             } else {
 364                 if (Files.isRegularFile(Paths.get(javaHome, "lib", "modules")))
 365                     throw new IllegalArgumentException("Invalid java.home: " + javaHome);
 366 
 367                 // alternate java.home
 368                 Map<String, String> env = new HashMap<>();
 369                 env.put("java.home", javaHome);
 370                 // a remote run-time image
 371                 this.fileSystem = FileSystems.newFileSystem(URI.create("jrt:/"), env);
 372                 this.root = fileSystem.getPath("/modules");
 373                 this.systemModules = walk(root);
 374             }
 375         }
 376 
 377         private Map<String, ModuleReference> walk(Path root) {
 378             try (Stream<Path> stream = Files.walk(root, 1)) {
 379                 return stream.filter(path -> !path.equals(root))
 380                              .map(this::toModuleReference)
 381                              .collect(toMap(mref -> mref.descriptor().name(),
 382                                             Function.identity()));
 383             } catch (IOException e) {
 384                 throw new UncheckedIOException(e);
 385             }
 386         }
 387 
 388         private ModuleReference toModuleReference(Path path) {
 389             Path minfo = path.resolve(MODULE_INFO);
 390             try (InputStream in = Files.newInputStream(minfo);
 391                  BufferedInputStream bin = new BufferedInputStream(in)) {
 392 
 393                 ModuleDescriptor descriptor = dropHashes(ModuleDescriptor.read(bin));
 394                 String mn = descriptor.name();
 395                 URI uri = URI.create("jrt:/" + path.getFileName().toString());
 396                 Supplier<ModuleReader> readerSupplier = () -> new ModuleReader() {
 397                     @Override
 398                     public Optional<URI> find(String name) throws IOException {
 399                         return name.equals(mn)
 400                             ? Optional.of(uri) : Optional.empty();
 401                     }
 402 
 403                     @Override
 404                     public Stream<String> list() {
 405                         return Stream.empty();
 406                     }
 407 
 408                     @Override
 409                     public void close() {
 410                     }
 411                 };
 412 
 413                 return new ModuleReference(descriptor, uri) {
 414                     @Override
 415                     public ModuleReader open() {
 416                         return readerSupplier.get();
 417                     }
 418                 };
 419             } catch (IOException e) {
 420                 throw new UncheckedIOException(e);
 421             }
 422         }
 423 
 424         private ModuleDescriptor dropHashes(ModuleDescriptor md) {
 425             ModuleDescriptor.Builder builder = ModuleDescriptor.newModule(md.name());
 426             md.requires().forEach(builder::requires);
 427             md.exports().forEach(builder::exports);
 428             md.opens().forEach(builder::opens);
 429             md.provides().stream().forEach(builder::provides);
 430             md.uses().stream().forEach(builder::uses);
 431             builder.packages(md.packages());
 432             return builder.build();
 433         }
 434 
 435         @Override
 436         public Set<ModuleReference> findAll() {
 437             return systemModules.values().stream().collect(toSet());
 438         }
 439 
 440         @Override
 441         public Optional<ModuleReference> find(String mn) {
 442             return systemModules.containsKey(mn)
 443                     ? Optional.of(systemModules.get(mn)) : Optional.empty();
 444         }
 445 
 446         public Stream<String> moduleNames() {
 447             return systemModules.values().stream()
 448                 .map(mref -> mref.descriptor().name());
 449         }
 450 
 451         public ClassFileReader getClassReader(String modulename) throws IOException {
 452             Path mp = root.resolve(modulename);
 453             if (Files.exists(mp) && Files.isDirectory(mp)) {
 454                 return ClassFileReader.newInstance(fileSystem, mp);
 455             } else {
 456                 throw new FileNotFoundException(mp.toString());
 457             }
 458         }
 459 
 460         public Set<String> defaultSystemRoots() {
 461             Set<String> roots = new HashSet<>();
 462             boolean hasJava = false;
 463             if (systemModules.containsKey(JAVA_SE)) {
 464                 // java.se is a system module
 465                 hasJava = true;
 466                 roots.add(JAVA_SE);
 467             }
 468 
 469             for (ModuleReference mref : systemModules.values()) {
 470                 String mn = mref.descriptor().name();
 471                 if (hasJava && mn.startsWith("java."))
 472                     continue;
 473 
 474                 // add as root if observable and exports at least one package
 475                 ModuleDescriptor descriptor = mref.descriptor();
 476                 for (ModuleDescriptor.Exports e : descriptor.exports()) {
 477                     if (!e.isQualified()) {
 478                         roots.add(mn);
 479                         break;
 480                     }
 481                 }
 482             }
 483             return roots;
 484         }
 485     }
 486 
 487     public static class Builder {
 488 
 489         final SystemModuleFinder systemModulePath;
 490         final Set<String> rootModules = new HashSet<>();
 491         final List<Archive> initialArchives = new ArrayList<>();
 492         final List<Path> paths = new ArrayList<>();
 493         final List<Path> classPaths = new ArrayList<>();
 494 
 495         ModuleFinder upgradeModulePath;
 496         ModuleFinder appModulePath;
 497         boolean addAllApplicationModules;
 498         boolean addAllDefaultModules;
 499         boolean addAllSystemModules;
 500         boolean allModules;
 501         Runtime.Version version;
 502 
 503         public Builder() {
 504             this.systemModulePath = new SystemModuleFinder();
 505         }
 506 
 507         public Builder(String javaHome) throws IOException {
 508             this.systemModulePath = SystemModuleFinder.JAVA_HOME.equals(javaHome)
 509                 ? new SystemModuleFinder()
 510                 : new SystemModuleFinder(javaHome);
 511         }
 512 
 513         public Builder upgradeModulePath(String upgradeModulePath) {
 514             this.upgradeModulePath = createModulePathFinder(upgradeModulePath);
 515             return this;
 516         }
 517 
 518         public Builder appModulePath(String modulePath) {
 519             this.appModulePath = createModulePathFinder(modulePath);
 520             return this;
 521         }
 522 
 523         public Builder addmods(Set<String> addmods) {
 524             for (String mn : addmods) {
 525                 switch (mn) {
 526                     case ALL_MODULE_PATH:
 527                         this.addAllApplicationModules = true;
 528                         break;
 529                     case ALL_DEFAULT:
 530                         this.addAllDefaultModules = true;
 531                         break;
 532                     case ALL_SYSTEM:
 533                         this.addAllSystemModules = true;
 534                         break;
 535                     default:
 536                         this.rootModules.add(mn);
 537                 }
 538             }
 539             return this;
 540         }
 541 
 542         /*
 543          * This method is for --check option to find all target modules specified
 544          * in qualified exports.
 545          *
 546          * Include all system modules and modules found on modulepath
 547          */
 548         public Builder allModules() {
 549             this.allModules = true;
 550             return this;
 551         }
 552 
 553         public Builder multiRelease(Runtime.Version version) {
 554             this.version = version;
 555             return this;
 556         }
 557 
 558         public Builder addRoot(Path path) {
 559             Archive archive = Archive.getInstance(path, version);
 560             if (archive.contains(MODULE_INFO)) {
 561                 paths.add(path);
 562             } else {
 563                 initialArchives.add(archive);
 564             }
 565             return this;
 566         }
 567 
 568         public Builder addClassPath(String classPath) {
 569             this.classPaths.addAll(getClassPaths(classPath));
 570             return this;
 571         }
 572 
 573         public JdepsConfiguration build() throws  IOException {
 574             ModuleFinder finder = systemModulePath;
 575             if (upgradeModulePath != null) {
 576                 finder = ModuleFinder.compose(upgradeModulePath, systemModulePath);
 577             }
 578             if (appModulePath != null) {
 579                 finder = ModuleFinder.compose(finder, appModulePath);
 580             }
 581             if (!paths.isEmpty()) {
 582                 ModuleFinder otherModulePath = ModuleFinder.of(paths.toArray(new Path[0]));
 583 
 584                 finder = ModuleFinder.compose(finder, otherModulePath);
 585                 // add modules specified on command-line (convenience) as root set
 586                 otherModulePath.findAll().stream()
 587                         .map(mref -> mref.descriptor().name())
 588                         .forEach(rootModules::add);
 589             }
 590 
 591             if ((addAllApplicationModules || allModules) && appModulePath != null) {
 592                 appModulePath.findAll().stream()
 593                     .map(mref -> mref.descriptor().name())
 594                     .forEach(rootModules::add);
 595             }
 596 
 597             // no archive is specified for analysis
 598             // add all system modules as root if --add-modules ALL-SYSTEM is specified
 599             if (addAllSystemModules && rootModules.isEmpty() &&
 600                     initialArchives.isEmpty() && classPaths.isEmpty()) {
 601                 systemModulePath.findAll()
 602                     .stream()
 603                     .map(mref -> mref.descriptor().name())
 604                     .forEach(rootModules::add);
 605             }
 606 
 607             return new JdepsConfiguration(systemModulePath,
 608                                           finder,
 609                                           rootModules,
 610                                           classPaths,
 611                                           initialArchives,
 612                                           addAllDefaultModules,
 613                                           allModules,
 614                                           version);
 615         }
 616 
 617         private static ModuleFinder createModulePathFinder(String mpaths) {
 618             if (mpaths == null) {
 619                 return null;
 620             } else {
 621                 String[] dirs = mpaths.split(File.pathSeparator);
 622                 Path[] paths = new Path[dirs.length];
 623                 int i = 0;
 624                 for (String dir : dirs) {
 625                     paths[i++] = Paths.get(dir);
 626                 }
 627                 return ModuleFinder.of(paths);
 628             }
 629         }
 630 
 631         /*
 632          * Returns the list of Archive specified in cpaths and not included
 633          * initialArchives
 634          */
 635         private List<Path> getClassPaths(String cpaths) {
 636             if (cpaths.isEmpty()) {
 637                 return Collections.emptyList();
 638             }
 639             List<Path> paths = new ArrayList<>();
 640             for (String p : cpaths.split(File.pathSeparator)) {
 641                 if (p.length() > 0) {
 642                     // wildcard to parse all JAR files e.g. -classpath dir/*
 643                     int i = p.lastIndexOf(".*");
 644                     if (i > 0) {
 645                         Path dir = Paths.get(p.substring(0, i));
 646                         try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.jar")) {
 647                             for (Path entry : stream) {
 648                                 paths.add(entry);
 649                             }
 650                         } catch (IOException e) {
 651                             throw new UncheckedIOException(e);
 652                         }
 653                     } else {
 654                         paths.add(Paths.get(p));
 655                     }
 656                 }
 657             }
 658             return paths;
 659         }
 660     }
 661 
 662 }