1 /*
   2  * Copyright (c) 2012, 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 
  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 list of packages that split between resolved module and
 211      * unnamed module
 212      */
 213     public Map<String, Set<String>> splitPackages() {
 214         Set<String> splitPkgs = packageToModule.keySet().stream()
 215                                        .filter(packageToUnnamedModule::containsKey)
 216                                        .collect(toSet());
 217         if (splitPkgs.isEmpty())
 218             return Collections.emptyMap();
 219 
 220         return splitPkgs.stream().collect(toMap(Function.identity(), (pn) -> {
 221             Set<String> sources = new LinkedHashSet<>();
 222             sources.add(packageToModule.get(pn).getModule().location().toString());
 223             packageToUnnamedModule.get(pn).stream()
 224                 .map(Archive::getPathName)
 225                 .forEach(sources::add);
 226             return sources;
 227         }));
 228     }
 229 
 230     /**
 231      * Returns an optional archive containing the given Location
 232      */
 233     public Optional<Archive> findClass(Dependency.Location location) {
 234         String name = location.getName();
 235         int i = name.lastIndexOf('/');
 236         String pn = i > 0 ? name.substring(0, i).replace('/', '.') : "";
 237         Archive archive = packageToModule.get(pn);
 238         if (archive != null) {
 239             return archive.contains(name + ".class")
 240                         ? Optional.of(archive)
 241                         : Optional.empty();
 242         }
 243 
 244         if (packageToUnnamedModule.containsKey(pn)) {
 245             return packageToUnnamedModule.get(pn).stream()
 246                     .filter(a -> a.contains(name + ".class"))
 247                     .findFirst();
 248         }
 249         return Optional.empty();
 250     }
 251 
 252     /**
 253      * Returns the list of Modules that can be found in the specified
 254      * module paths.
 255      */
 256     public Map<String, Module> getModules() {
 257         return nameToModule;
 258     }
 259 
 260     /**
 261      * Returns Configuration with the given roots
 262      */
 263     public Configuration resolve(Set<String> roots) {
 264         if (roots.isEmpty())
 265             throw new IllegalArgumentException("empty roots");
 266 
 267         return Configuration.empty()
 268                     .resolve(finder, ModuleFinder.of(), roots);
 269     }
 270 
 271     public List<Archive> classPathArchives() {
 272         return classpathArchives;
 273     }
 274 
 275     public List<Archive> initialArchives() {
 276         return initialArchives;
 277     }
 278 
 279     public Set<Module> rootModules() {
 280         return rootModules;
 281     }
 282 
 283     public Module toModule(ModuleReference mref) {
 284         try {
 285             String mn = mref.descriptor().name();
 286             URI location = mref.location().orElseThrow(FileNotFoundException::new);
 287             ModuleDescriptor md = mref.descriptor();
 288             Module.Builder builder = new Module.Builder(md, system.find(mn).isPresent());
 289 
 290             final ClassFileReader reader;
 291             if (location.getScheme().equals("jrt")) {
 292                 reader = system.getClassReader(mn);
 293             } else {
 294                 reader = ClassFileReader.newInstance(Paths.get(location), version);
 295             }
 296 
 297             builder.classes(reader);
 298             builder.location(location);
 299 
 300             return builder.build();
 301         } catch (IOException e) {
 302             throw new UncheckedIOException(e);
 303         }
 304     }
 305 
 306     public Runtime.Version getVersion() {
 307         return version;
 308     }
 309 
 310     /*
 311      * Close all archives e.g. JarFile
 312      */
 313     @Override
 314     public void close() throws IOException {
 315         for (Archive archive : initialArchives)
 316             archive.close();
 317         for (Archive archive : classpathArchives)
 318             archive.close();
 319         for (Module module : nameToModule.values())
 320             module.close();
 321     }
 322 
 323     static class SystemModuleFinder implements ModuleFinder {
 324         private static final String JAVA_HOME = System.getProperty("java.home");
 325         private static final String JAVA_SE = "java.se";
 326 
 327         private final FileSystem fileSystem;
 328         private final Path root;
 329         private final Map<String, ModuleReference> systemModules;
 330 
 331         SystemModuleFinder() {
 332             if (Files.isRegularFile(Paths.get(JAVA_HOME, "lib", "modules"))) {
 333                 // jrt file system
 334                 this.fileSystem = FileSystems.getFileSystem(URI.create("jrt:/"));
 335                 this.root = fileSystem.getPath("/modules");
 336                 this.systemModules = walk(root);
 337             } else {
 338                 // exploded image
 339                 this.fileSystem = FileSystems.getDefault();
 340                 root = Paths.get(JAVA_HOME, "modules");
 341                 this.systemModules = ModuleFinder.ofSystem().findAll().stream()
 342                     .collect(toMap(mref -> mref.descriptor().name(), Function.identity()));
 343             }
 344         }
 345 
 346         SystemModuleFinder(String javaHome) throws IOException {
 347             if (javaHome == null) {
 348                 // --system none
 349                 this.fileSystem = null;
 350                 this.root = null;
 351                 this.systemModules = Collections.emptyMap();
 352             } else {
 353                 if (Files.isRegularFile(Paths.get(javaHome, "lib", "modules")))
 354                     throw new IllegalArgumentException("Invalid java.home: " + javaHome);
 355 
 356                 // alternate java.home
 357                 Map<String, String> env = new HashMap<>();
 358                 env.put("java.home", javaHome);
 359                 // a remote run-time image
 360                 this.fileSystem = FileSystems.newFileSystem(URI.create("jrt:/"), env);
 361                 this.root = fileSystem.getPath("/modules");
 362                 this.systemModules = walk(root);
 363             }
 364         }
 365 
 366         private Map<String, ModuleReference> walk(Path root) {
 367             try (Stream<Path> stream = Files.walk(root, 1)) {
 368                 return stream.filter(path -> !path.equals(root))
 369                              .map(this::toModuleReference)
 370                              .collect(toMap(mref -> mref.descriptor().name(),
 371                                             Function.identity()));
 372             } catch (IOException e) {
 373                 throw new UncheckedIOException(e);
 374             }
 375         }
 376 
 377         private ModuleReference toModuleReference(Path path) {
 378             Path minfo = path.resolve(MODULE_INFO);
 379             try (InputStream in = Files.newInputStream(minfo);
 380                  BufferedInputStream bin = new BufferedInputStream(in)) {
 381 
 382                 ModuleDescriptor descriptor = dropHashes(ModuleDescriptor.read(bin));
 383                 String mn = descriptor.name();
 384                 URI uri = URI.create("jrt:/" + path.getFileName().toString());
 385                 Supplier<ModuleReader> readerSupplier = () -> new ModuleReader() {
 386                     @Override
 387                     public Optional<URI> find(String name) throws IOException {
 388                         return name.equals(mn)
 389                             ? Optional.of(uri) : Optional.empty();
 390                     }
 391 
 392                     @Override
 393                     public Stream<String> list() {
 394                         return Stream.empty();
 395                     }
 396 
 397                     @Override
 398                     public void close() {
 399                     }
 400                 };
 401 
 402                 return new ModuleReference(descriptor, uri) {
 403                     @Override
 404                     public ModuleReader open() {
 405                         return readerSupplier.get();
 406                     }
 407                 };
 408             } catch (IOException e) {
 409                 throw new UncheckedIOException(e);
 410             }
 411         }
 412 
 413         private ModuleDescriptor dropHashes(ModuleDescriptor md) {
 414             ModuleDescriptor.Builder builder = ModuleDescriptor.newModule(md.name());
 415             md.requires().forEach(builder::requires);
 416             md.exports().forEach(builder::exports);
 417             md.opens().forEach(builder::opens);
 418             md.provides().stream().forEach(builder::provides);
 419             md.uses().stream().forEach(builder::uses);
 420             builder.packages(md.packages());
 421             return builder.build();
 422         }
 423 
 424         @Override
 425         public Set<ModuleReference> findAll() {
 426             return systemModules.values().stream().collect(toSet());
 427         }
 428 
 429         @Override
 430         public Optional<ModuleReference> find(String mn) {
 431             return systemModules.containsKey(mn)
 432                     ? Optional.of(systemModules.get(mn)) : Optional.empty();
 433         }
 434 
 435         public Stream<String> moduleNames() {
 436             return systemModules.values().stream()
 437                 .map(mref -> mref.descriptor().name());
 438         }
 439 
 440         public ClassFileReader getClassReader(String modulename) throws IOException {
 441             Path mp = root.resolve(modulename);
 442             if (Files.exists(mp) && Files.isDirectory(mp)) {
 443                 return ClassFileReader.newInstance(fileSystem, mp);
 444             } else {
 445                 throw new FileNotFoundException(mp.toString());
 446             }
 447         }
 448 
 449         public Set<String> defaultSystemRoots() {
 450             Set<String> roots = new HashSet<>();
 451             boolean hasJava = false;
 452             if (systemModules.containsKey(JAVA_SE)) {
 453                 // java.se is a system module
 454                 hasJava = true;
 455                 roots.add(JAVA_SE);
 456             }
 457 
 458             for (ModuleReference mref : systemModules.values()) {
 459                 String mn = mref.descriptor().name();
 460                 if (hasJava && mn.startsWith("java."))
 461                     continue;
 462 
 463                 // add as root if observable and exports at least one package
 464                 ModuleDescriptor descriptor = mref.descriptor();
 465                 for (ModuleDescriptor.Exports e : descriptor.exports()) {
 466                     if (!e.isQualified()) {
 467                         roots.add(mn);
 468                         break;
 469                     }
 470                 }
 471             }
 472             return roots;
 473         }
 474     }
 475 
 476     public static class Builder {
 477 
 478         final SystemModuleFinder systemModulePath;
 479         final Set<String> rootModules = new HashSet<>();
 480         final List<Archive> initialArchives = new ArrayList<>();
 481         final List<Path> paths = new ArrayList<>();
 482         final List<Path> classPaths = new ArrayList<>();
 483 
 484         ModuleFinder upgradeModulePath;
 485         ModuleFinder appModulePath;
 486         boolean addAllApplicationModules;
 487         boolean addAllDefaultModules;
 488         boolean addAllSystemModules;
 489         boolean allModules;
 490         Runtime.Version version;
 491 
 492         public Builder() {
 493             this.systemModulePath = new SystemModuleFinder();
 494         }
 495 
 496         public Builder(String javaHome) throws IOException {
 497             this.systemModulePath = SystemModuleFinder.JAVA_HOME.equals(javaHome)
 498                 ? new SystemModuleFinder()
 499                 : new SystemModuleFinder(javaHome);
 500         }
 501 
 502         public Builder upgradeModulePath(String upgradeModulePath) {
 503             this.upgradeModulePath = createModulePathFinder(upgradeModulePath);
 504             return this;
 505         }
 506 
 507         public Builder appModulePath(String modulePath) {
 508             this.appModulePath = createModulePathFinder(modulePath);
 509             return this;
 510         }
 511 
 512         public Builder addmods(Set<String> addmods) {
 513             for (String mn : addmods) {
 514                 switch (mn) {
 515                     case ALL_MODULE_PATH:
 516                         this.addAllApplicationModules = true;
 517                         break;
 518                     case ALL_DEFAULT:
 519                         this.addAllDefaultModules = true;
 520                         break;
 521                     case ALL_SYSTEM:
 522                         this.addAllSystemModules = true;
 523                         break;
 524                     default:
 525                         this.rootModules.add(mn);
 526                 }
 527             }
 528             return this;
 529         }
 530 
 531         /*
 532          * This method is for --check option to find all target modules specified
 533          * in qualified exports.
 534          *
 535          * Include all system modules and modules found on modulepath
 536          */
 537         public Builder allModules() {
 538             this.allModules = true;
 539             return this;
 540         }
 541 
 542         public Builder multiRelease(Runtime.Version version) {
 543             this.version = version;
 544             return this;
 545         }
 546 
 547         public Builder addRoot(Path path) {
 548             Archive archive = Archive.getInstance(path, version);
 549             if (archive.contains(MODULE_INFO)) {
 550                 paths.add(path);
 551             } else {
 552                 initialArchives.add(archive);
 553             }
 554             return this;
 555         }
 556 
 557         public Builder addClassPath(String classPath) {
 558             this.classPaths.addAll(getClassPaths(classPath));
 559             return this;
 560         }
 561 
 562         public JdepsConfiguration build() throws  IOException {
 563             ModuleFinder finder = systemModulePath;
 564             if (upgradeModulePath != null) {
 565                 finder = ModuleFinder.compose(upgradeModulePath, systemModulePath);
 566             }
 567             if (appModulePath != null) {
 568                 finder = ModuleFinder.compose(finder, appModulePath);
 569             }
 570             if (!paths.isEmpty()) {
 571                 ModuleFinder otherModulePath = ModuleFinder.of(paths.toArray(new Path[0]));
 572 
 573                 finder = ModuleFinder.compose(finder, otherModulePath);
 574                 // add modules specified on command-line (convenience) as root set
 575                 otherModulePath.findAll().stream()
 576                         .map(mref -> mref.descriptor().name())
 577                         .forEach(rootModules::add);
 578             }
 579 
 580             if ((addAllApplicationModules || allModules) && appModulePath != null) {
 581                 appModulePath.findAll().stream()
 582                     .map(mref -> mref.descriptor().name())
 583                     .forEach(rootModules::add);
 584             }
 585 
 586             // no archive is specified for analysis
 587             // add all system modules as root if --add-modules ALL-SYSTEM is specified
 588             if (addAllSystemModules && rootModules.isEmpty() &&
 589                     initialArchives.isEmpty() && classPaths.isEmpty()) {
 590                 systemModulePath.findAll()
 591                     .stream()
 592                     .map(mref -> mref.descriptor().name())
 593                     .forEach(rootModules::add);
 594             }
 595 
 596             return new JdepsConfiguration(systemModulePath,
 597                                           finder,
 598                                           rootModules,
 599                                           classPaths,
 600                                           initialArchives,
 601                                           addAllDefaultModules,
 602                                           allModules,
 603                                           version);
 604         }
 605 
 606         private static ModuleFinder createModulePathFinder(String mpaths) {
 607             if (mpaths == null) {
 608                 return null;
 609             } else {
 610                 String[] dirs = mpaths.split(File.pathSeparator);
 611                 Path[] paths = new Path[dirs.length];
 612                 int i = 0;
 613                 for (String dir : dirs) {
 614                     paths[i++] = Paths.get(dir);
 615                 }
 616                 return ModuleFinder.of(paths);
 617             }
 618         }
 619 
 620         /*
 621          * Returns the list of Archive specified in cpaths and not included
 622          * initialArchives
 623          */
 624         private List<Path> getClassPaths(String cpaths) {
 625             if (cpaths.isEmpty()) {
 626                 return Collections.emptyList();
 627             }
 628             List<Path> paths = new ArrayList<>();
 629             for (String p : cpaths.split(File.pathSeparator)) {
 630                 if (p.length() > 0) {
 631                     // wildcard to parse all JAR files e.g. -classpath dir/*
 632                     int i = p.lastIndexOf(".*");
 633                     if (i > 0) {
 634                         Path dir = Paths.get(p.substring(0, i));
 635                         try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.jar")) {
 636                             for (Path entry : stream) {
 637                                 paths.add(entry);
 638                             }
 639                         } catch (IOException e) {
 640                             throw new UncheckedIOException(e);
 641                         }
 642                     } else {
 643                         paths.add(Paths.get(p));
 644                     }
 645                 }
 646             }
 647             return paths;
 648         }
 649     }
 650 
 651 }