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