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