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                 .resolveRequires(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                     .resolveRequires(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.module(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 
 432             Set<String> concealed = new HashSet<>(md.packages());
 433             md.exports().stream().map(Exports::source).forEach(concealed::remove);
 434             md.opens().stream().map(Opens::source).forEach(concealed::remove);
 435             concealed.forEach(builder::contains);
 436 
 437             return builder.build();
 438         }
 439 
 440         @Override
 441         public Set<ModuleReference> findAll() {
 442             return systemModules.values().stream().collect(toSet());
 443         }
 444 
 445         @Override
 446         public Optional<ModuleReference> find(String mn) {
 447             return systemModules.containsKey(mn)
 448                     ? Optional.of(systemModules.get(mn)) : Optional.empty();
 449         }
 450 
 451         public Stream<String> moduleNames() {
 452             return systemModules.values().stream()
 453                 .map(mref -> mref.descriptor().name());
 454         }
 455 
 456         public ClassFileReader getClassReader(String modulename) throws IOException {
 457             Path mp = root.resolve(modulename);
 458             if (Files.exists(mp) && Files.isDirectory(mp)) {
 459                 return ClassFileReader.newInstance(fileSystem, mp);
 460             } else {
 461                 throw new FileNotFoundException(mp.toString());
 462             }
 463         }
 464 
 465         public Set<String> defaultSystemRoots() {
 466             Set<String> roots = new HashSet<>();
 467             boolean hasJava = false;
 468             if (systemModules.containsKey(JAVA_SE)) {
 469                 // java.se is a system module
 470                 hasJava = true;
 471                 roots.add(JAVA_SE);
 472             }
 473 
 474             for (ModuleReference mref : systemModules.values()) {
 475                 String mn = mref.descriptor().name();
 476                 if (hasJava && mn.startsWith("java."))
 477                     continue;
 478 
 479                 // add as root if observable and exports at least one package
 480                 ModuleDescriptor descriptor = mref.descriptor();
 481                 for (ModuleDescriptor.Exports e : descriptor.exports()) {
 482                     if (!e.isQualified()) {
 483                         roots.add(mn);
 484                         break;
 485                     }
 486                 }
 487             }
 488             return roots;
 489         }
 490     }
 491 
 492     public static class Builder {
 493 
 494         final SystemModuleFinder systemModulePath;
 495         final Set<String> rootModules = new HashSet<>();
 496         final List<Archive> initialArchives = new ArrayList<>();
 497         final List<Path> paths = new ArrayList<>();
 498         final List<Path> classPaths = new ArrayList<>();
 499 
 500         ModuleFinder upgradeModulePath;
 501         ModuleFinder appModulePath;
 502         boolean addAllApplicationModules;
 503         boolean addAllDefaultModules;
 504         boolean addAllSystemModules;
 505         boolean allModules;
 506         Runtime.Version version;
 507 
 508         public Builder() {
 509             this.systemModulePath = new SystemModuleFinder();
 510         }
 511 
 512         public Builder(String javaHome) throws IOException {
 513             this.systemModulePath = SystemModuleFinder.JAVA_HOME.equals(javaHome)
 514                 ? new SystemModuleFinder()
 515                 : new SystemModuleFinder(javaHome);
 516         }
 517 
 518         public Builder upgradeModulePath(String upgradeModulePath) {
 519             this.upgradeModulePath = createModulePathFinder(upgradeModulePath);
 520             return this;
 521         }
 522 
 523         public Builder appModulePath(String modulePath) {
 524             this.appModulePath = createModulePathFinder(modulePath);
 525             return this;
 526         }
 527 
 528         public Builder addmods(Set<String> addmods) {
 529             for (String mn : addmods) {
 530                 switch (mn) {
 531                     case ALL_MODULE_PATH:
 532                         this.addAllApplicationModules = true;
 533                         break;
 534                     case ALL_DEFAULT:
 535                         this.addAllDefaultModules = true;
 536                         break;
 537                     case ALL_SYSTEM:
 538                         this.addAllSystemModules = true;
 539                         break;
 540                     default:
 541                         this.rootModules.add(mn);
 542                 }
 543             }
 544             return this;
 545         }
 546 
 547         /*
 548          * This method is for --check option to find all target modules specified
 549          * in qualified exports.
 550          *
 551          * Include all system modules and modules found on modulepath
 552          */
 553         public Builder allModules() {
 554             this.allModules = true;
 555             return this;
 556         }
 557 
 558         public Builder multiRelease(Runtime.Version version) {
 559             this.version = version;
 560             return this;
 561         }
 562 
 563         public Builder addRoot(Path path) {
 564             Archive archive = Archive.getInstance(path, version);
 565             if (archive.contains(MODULE_INFO)) {
 566                 paths.add(path);
 567             } else {
 568                 initialArchives.add(archive);
 569             }
 570             return this;
 571         }
 572 
 573         public Builder addClassPath(String classPath) {
 574             this.classPaths.addAll(getClassPaths(classPath));
 575             return this;
 576         }
 577 
 578         public JdepsConfiguration build() throws  IOException {
 579             ModuleFinder finder = systemModulePath;
 580             if (upgradeModulePath != null) {
 581                 finder = ModuleFinder.compose(upgradeModulePath, systemModulePath);
 582             }
 583             if (appModulePath != null) {
 584                 finder = ModuleFinder.compose(finder, appModulePath);
 585             }
 586             if (!paths.isEmpty()) {
 587                 ModuleFinder otherModulePath = ModuleFinder.of(paths.toArray(new Path[0]));
 588 
 589                 finder = ModuleFinder.compose(finder, otherModulePath);
 590                 // add modules specified on command-line (convenience) as root set
 591                 otherModulePath.findAll().stream()
 592                         .map(mref -> mref.descriptor().name())
 593                         .forEach(rootModules::add);
 594             }
 595 
 596             if ((addAllApplicationModules || allModules) && appModulePath != null) {
 597                 appModulePath.findAll().stream()
 598                     .map(mref -> mref.descriptor().name())
 599                     .forEach(rootModules::add);
 600             }
 601 
 602             // no archive is specified for analysis
 603             // add all system modules as root if --add-modules ALL-SYSTEM is specified
 604             if (addAllSystemModules && rootModules.isEmpty() &&
 605                     initialArchives.isEmpty() && classPaths.isEmpty()) {
 606                 systemModulePath.findAll()
 607                     .stream()
 608                     .map(mref -> mref.descriptor().name())
 609                     .forEach(rootModules::add);
 610             }
 611 
 612             return new JdepsConfiguration(systemModulePath,
 613                                           finder,
 614                                           rootModules,
 615                                           classPaths,
 616                                           initialArchives,
 617                                           addAllDefaultModules,
 618                                           allModules,
 619                                           version);
 620         }
 621 
 622         private static ModuleFinder createModulePathFinder(String mpaths) {
 623             if (mpaths == null) {
 624                 return null;
 625             } else {
 626                 String[] dirs = mpaths.split(File.pathSeparator);
 627                 Path[] paths = new Path[dirs.length];
 628                 int i = 0;
 629                 for (String dir : dirs) {
 630                     paths[i++] = Paths.get(dir);
 631                 }
 632                 return ModuleFinder.of(paths);
 633             }
 634         }
 635 
 636         /*
 637          * Returns the list of Archive specified in cpaths and not included
 638          * initialArchives
 639          */
 640         private List<Path> getClassPaths(String cpaths) {
 641             if (cpaths.isEmpty()) {
 642                 return Collections.emptyList();
 643             }
 644             List<Path> paths = new ArrayList<>();
 645             for (String p : cpaths.split(File.pathSeparator)) {
 646                 if (p.length() > 0) {
 647                     // wildcard to parse all JAR files e.g. -classpath dir/*
 648                     int i = p.lastIndexOf(".*");
 649                     if (i > 0) {
 650                         Path dir = Paths.get(p.substring(0, i));
 651                         try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.jar")) {
 652                             for (Path entry : stream) {
 653                                 paths.add(entry);
 654                             }
 655                         } catch (IOException e) {
 656                             throw new UncheckedIOException(e);
 657                         }
 658                     } else {
 659                         paths.add(Paths.get(p));
 660                     }
 661                 }
 662             }
 663             return paths;
 664         }
 665     }
 666 
 667 }