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