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