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.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.Stream;
  66 
  67 public class JdepsConfiguration implements AutoCloseable {
  68     // the token for "all modules on the module path"
  69     public static final String ALL_MODULE_PATH = "ALL-MODULE-PATH";
  70     public static final String ALL_DEFAULT = "ALL-DEFAULT";
  71     public static final String MODULE_INFO = "module-info.class";
  72 
  73     private final SystemModuleFinder system;
  74     private final ModuleFinder finder;
  75 
  76     private final Map<String, Module> nameToModule = new LinkedHashMap<>();
  77     private final Map<String, Module> packageToModule = new HashMap<>();
  78     private final Map<String, List<Archive>> packageToUnnamedModule = new HashMap<>();
  79 
  80     private final List<Archive> classpathArchives = new ArrayList<>();
  81     private final List<Archive> initialArchives = new ArrayList<>();
  82     private final Set<Module> rootModules = new HashSet<>();
  83     private final Configuration configuration;
  84 
  85     private JdepsConfiguration(SystemModuleFinder systemModulePath,
  86                                ModuleFinder finder,
  87                                Set<String> roots,
  88                                List<Path> classpaths,
  89                                List<Archive> initialArchives,
  90                                boolean allDefaultModules)
  91         throws IOException
  92     {
  93         trace("root: %s%n", roots);
  94 
  95         this.system = systemModulePath;
  96         this.finder = finder;
  97 
  98         // build root set for resolution
  99         Set<String> mods = new HashSet<>(roots);
 100 
 101         // add default modules to the root set
 102         // unnamed module
 103         if (!initialArchives.isEmpty() || !classpaths.isEmpty() ||
 104                 roots.isEmpty() || allDefaultModules) {
 105             mods.addAll(systemModulePath.defaultSystemRoots());
 106         }
 107 
 108         this.configuration = Configuration.empty()
 109                 .resolveRequires(finder, ModuleFinder.of(), mods);
 110 
 111         this.configuration.modules().stream()
 112                 .map(ResolvedModule::reference)
 113                 .forEach(this::addModuleReference);
 114 
 115         // packages in unnamed module
 116         initialArchives.forEach(archive -> {
 117             addPackagesInUnnamedModule(archive);
 118             this.initialArchives.add(archive);
 119         });
 120 
 121         // classpath archives
 122         for (Path p : classpaths) {
 123             if (Files.exists(p)) {
 124                 Archive archive = Archive.getInstance(p);
 125                 addPackagesInUnnamedModule(archive);
 126                 classpathArchives.add(archive);
 127             }
 128         }
 129 
 130         // all roots specified in -addmods or -m are included
 131         // as the initial set for analysis.
 132         roots.stream()
 133              .map(nameToModule::get)
 134              .forEach(this.rootModules::add);
 135 
 136         initProfiles();
 137 
 138         trace("resolved modules: %s%n", nameToModule.keySet().stream()
 139                 .sorted().collect(joining("\n", "\n", "")));
 140     }
 141 
 142     private void initProfiles() {
 143         // other system modules are not observed and not added in nameToModule map
 144         Map<String, Module> systemModules =
 145             system.moduleNames()
 146                 .collect(toMap(Function.identity(), (mn) -> {
 147                     Module m = nameToModule.get(mn);
 148                     if (m == null) {
 149                         ModuleReference mref = finder.find(mn).get();
 150                         m = toModule(mref);
 151                     }
 152                     return m;
 153                 }));
 154         Profile.init(systemModules);
 155     }
 156 
 157     private void addModuleReference(ModuleReference mref) {
 158         Module module = toModule(mref);
 159         nameToModule.put(mref.descriptor().name(), module);
 160         mref.descriptor().packages()
 161             .forEach(pn -> packageToModule.putIfAbsent(pn, module));
 162     }
 163 
 164     private void addPackagesInUnnamedModule(Archive archive) {
 165         archive.reader().entries().stream()
 166                .filter(e -> e.endsWith(".class") && !e.equals(MODULE_INFO))
 167                .map(this::toPackageName)
 168                .distinct()
 169                .forEach(pn -> packageToUnnamedModule
 170                    .computeIfAbsent(pn, _n -> new ArrayList<>()).add(archive));
 171     }
 172 
 173     private String toPackageName(String name) {
 174         int i = name.lastIndexOf('/');
 175         return i > 0 ? name.replace('/', '.').substring(0, i) : "";
 176     }
 177 
 178     public Optional<Module> findModule(String name) {
 179         Objects.requireNonNull(name);
 180         Module m = nameToModule.get(name);
 181         return m!= null ? Optional.of(m) : Optional.empty();
 182 
 183     }
 184 
 185     public Optional<ModuleDescriptor> findModuleDescriptor(String name) {
 186         Objects.requireNonNull(name);
 187         Module m = nameToModule.get(name);
 188         return m!= null ? Optional.of(m.descriptor()) : Optional.empty();
 189     }
 190 
 191     boolean isSystem(Module m) {
 192         return system.find(m.name()).isPresent();
 193     }
 194 
 195     boolean isValidToken(String name) {
 196         return ALL_MODULE_PATH.equals(name) || ALL_DEFAULT.equals(name);
 197     }
 198 
 199     /**
 200      * Returns the modules that the given module can read
 201      */
 202     public Stream<Module> reads(Module module) {
 203         return configuration.findModule(module.name()).get()
 204             .reads().stream()
 205             .map(ResolvedModule::name)
 206             .map(nameToModule::get);
 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     public Stream<Module> resolve(Set<String> roots) {
 261         if (roots.isEmpty()) {
 262             return nameToModule.values().stream();
 263         } else {
 264             return Configuration.empty()
 265                     .resolveRequires(finder, ModuleFinder.of(), roots)
 266                     .modules().stream()
 267                     .map(ResolvedModule::name)
 268                     .map(nameToModule::get);
 269         }
 270     }
 271 
 272     public List<Archive> classPathArchives() {
 273         return classpathArchives;
 274     }
 275 
 276     public List<Archive> initialArchives() {
 277         return initialArchives;
 278     }
 279 
 280     public Set<Module> rootModules() {
 281         return rootModules;
 282     }
 283 
 284     public Module toModule(ModuleReference mref) {
 285         try {
 286             String mn = mref.descriptor().name();
 287             URI location = mref.location().orElseThrow(FileNotFoundException::new);
 288             ModuleDescriptor md = mref.descriptor();
 289             Module.Builder builder = new Module.Builder(md, system.find(mn).isPresent());
 290 
 291             final ClassFileReader reader;
 292             if (location.getScheme().equals("jrt")) {
 293                 reader = system.getClassReader(mn);
 294             } else {
 295                 reader = ClassFileReader.newInstance(Paths.get(location));
 296             }
 297 
 298             builder.classes(reader);
 299             builder.location(location);
 300 
 301             return builder.build();
 302         } catch (IOException e) {
 303             throw new UncheckedIOException(e);
 304         }
 305     }
 306 
 307     /*
 308      * Close all archives e.g. JarFile
 309      */
 310     @Override
 311     public void close() throws IOException {
 312         for (Archive archive : initialArchives)
 313             archive.close();
 314         for (Archive archive : classpathArchives)
 315             archive.close();
 316         for (Module module : nameToModule.values())
 317             module.close();
 318     }
 319 
 320     static class SystemModuleFinder implements ModuleFinder {
 321         private static final String JAVA_HOME = System.getProperty("java.home");
 322         private static final String JAVA_SE = "java.se";
 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 Supplier<>() {
 383                     @Override
 384                     public ModuleReader get() {
 385                         return new ModuleReader() {
 386                             @Override
 387                             public Optional<URI> find(String name) throws IOException {
 388                                 return name.equals(mn)
 389                                     ? Optional.of(uri) : Optional.empty();
 390                             }
 391 
 392                             @Override
 393                             public void close() throws IOException {
 394                             }
 395                         };
 396                     }
 397                 };
 398 
 399                 return new ModuleReference(descriptor, uri, readerSupplier);
 400             } catch (IOException e) {
 401                 throw new UncheckedIOException(e);
 402             }
 403         }
 404 
 405         private ModuleDescriptor dropHashes(ModuleDescriptor md) {
 406             ModuleDescriptor.Builder builder = new ModuleDescriptor.Builder(md.name());
 407             md.requires().forEach(builder::requires);
 408             md.exports().forEach(builder::exports);
 409             md.provides().values().stream().forEach(builder::provides);
 410             md.uses().stream().forEach(builder::uses);
 411             builder.conceals(md.conceals());
 412             return builder.build();
 413         }
 414 
 415         @Override
 416         public Set<ModuleReference> findAll() {
 417             return systemModules.values().stream().collect(toSet());
 418         }
 419 
 420         @Override
 421         public Optional<ModuleReference> find(String mn) {
 422             return systemModules.containsKey(mn)
 423                     ? Optional.of(systemModules.get(mn)) : Optional.empty();
 424         }
 425 
 426         public Stream<String> moduleNames() {
 427             return systemModules.values().stream()
 428                 .map(mref -> mref.descriptor().name());
 429         }
 430 
 431         public ClassFileReader getClassReader(String modulename) throws IOException {
 432             Path mp = root.resolve(modulename);
 433             if (Files.exists(mp) && Files.isDirectory(mp)) {
 434                 return ClassFileReader.newInstance(fileSystem, mp);
 435             } else {
 436                 throw new FileNotFoundException(mp.toString());
 437             }
 438         }
 439 
 440         public Set<String> defaultSystemRoots() {
 441             Set<String> roots = new HashSet<>();
 442             boolean hasJava = false;
 443             if (systemModules.containsKey(JAVA_SE)) {
 444                 // java.se is a system module
 445                 hasJava = true;
 446                 roots.add(JAVA_SE);
 447             }
 448 
 449             for (ModuleReference mref : systemModules.values()) {
 450                 String mn = mref.descriptor().name();
 451                 if (hasJava && mn.startsWith("java."))
 452                     continue;
 453 
 454                 // add as root if observable and exports at least one package
 455                 ModuleDescriptor descriptor = mref.descriptor();
 456                 for (ModuleDescriptor.Exports e : descriptor.exports()) {
 457                     if (!e.isQualified()) {
 458                         roots.add(mn);
 459                         break;
 460                     }
 461                 }
 462             }
 463             return roots;
 464         }
 465     }
 466 
 467     public static class Builder {
 468 
 469         final SystemModuleFinder systemModulePath;
 470         final Set<String> rootModules = new HashSet<>();
 471         final List<Archive> initialArchives = new ArrayList<>();
 472         final List<Path> paths = new ArrayList<>();
 473         final List<Path> classPaths = new ArrayList<>();
 474 
 475         ModuleFinder upgradeModulePath;
 476         ModuleFinder appModulePath;
 477         boolean addAllApplicationModules;
 478         boolean addAllDefaultModules;
 479 
 480         public Builder() {
 481             this.systemModulePath = new SystemModuleFinder();
 482         }
 483 
 484         public Builder(String javaHome) throws IOException {
 485             this.systemModulePath = SystemModuleFinder.JAVA_HOME.equals(javaHome)
 486                 ? new SystemModuleFinder()
 487                 : new SystemModuleFinder(javaHome);
 488         }
 489 
 490         public Builder upgradeModulePath(String upgradeModulePath) {
 491             this.upgradeModulePath = createModulePathFinder(upgradeModulePath);
 492             return this;
 493         }
 494 
 495         public Builder appModulePath(String modulePath) {
 496             this.appModulePath = createModulePathFinder(modulePath);
 497             return this;
 498         }
 499 
 500         public Builder addmods(Set<String> addmods) {
 501             for (String mn : addmods) {
 502                 switch (mn) {
 503                     case ALL_MODULE_PATH:
 504                         this.addAllApplicationModules = true;
 505                         break;
 506                     case ALL_DEFAULT:
 507                         this.addAllDefaultModules = true;
 508                         break;
 509                     default:
 510                         this.rootModules.add(mn);
 511                 }
 512             }
 513             return this;
 514         }
 515 
 516         /*
 517          * This method is for --check option to find all target modules specified
 518          * in qualified exports.
 519          *
 520          * Include all system modules and modules found on modulepath
 521          */
 522         public Builder allModules() {
 523             systemModulePath.moduleNames()
 524                             .forEach(this.rootModules::add);
 525             this.addAllApplicationModules = true;
 526             return this;
 527         }
 528 
 529         public Builder addRoot(Path path) {
 530             Archive archive = Archive.getInstance(path);
 531             if (archive.contains(MODULE_INFO)) {
 532                 paths.add(path);
 533             } else {
 534                 initialArchives.add(archive);
 535             }
 536             return this;
 537         }
 538 
 539         public Builder addClassPath(String classPath) {
 540             this.classPaths.addAll(getClassPaths(classPath));
 541             return this;
 542         }
 543 
 544         public JdepsConfiguration build() throws  IOException {
 545             ModuleFinder finder = systemModulePath;
 546             if (upgradeModulePath != null) {
 547                 finder = ModuleFinder.compose(upgradeModulePath, systemModulePath);
 548             }
 549             if (appModulePath != null) {
 550                 finder = ModuleFinder.compose(finder, appModulePath);
 551             }
 552             if (!paths.isEmpty()) {
 553                 ModuleFinder otherModulePath = ModuleFinder.of(paths.toArray(new Path[0]));
 554 
 555                 finder = ModuleFinder.compose(finder, otherModulePath);
 556                 // add modules specified on command-line (convenience) as root set
 557                 otherModulePath.findAll().stream()
 558                         .map(mref -> mref.descriptor().name())
 559                         .forEach(rootModules::add);
 560             }
 561             if (addAllApplicationModules && appModulePath != null) {
 562                 appModulePath.findAll().stream()
 563                     .map(mref -> mref.descriptor().name())
 564                     .forEach(rootModules::add);
 565             }
 566 
 567             return new JdepsConfiguration(systemModulePath,
 568                                           finder,
 569                                           rootModules,
 570                                           classPaths,
 571                                           initialArchives,
 572                                           addAllDefaultModules);
 573         }
 574 
 575         private static ModuleFinder createModulePathFinder(String mpaths) {
 576             if (mpaths == null) {
 577                 return null;
 578             } else {
 579                 String[] dirs = mpaths.split(File.pathSeparator);
 580                 Path[] paths = new Path[dirs.length];
 581                 int i = 0;
 582                 for (String dir : dirs) {
 583                     paths[i++] = Paths.get(dir);
 584                 }
 585                 return ModuleFinder.of(paths);
 586             }
 587         }
 588 
 589         /*
 590          * Returns the list of Archive specified in cpaths and not included
 591          * initialArchives
 592          */
 593         private List<Path> getClassPaths(String cpaths) {
 594             if (cpaths.isEmpty()) {
 595                 return Collections.emptyList();
 596             }
 597             List<Path> paths = new ArrayList<>();
 598             for (String p : cpaths.split(File.pathSeparator)) {
 599                 if (p.length() > 0) {
 600                     // wildcard to parse all JAR files e.g. -classpath dir/*
 601                     int i = p.lastIndexOf(".*");
 602                     if (i > 0) {
 603                         Path dir = Paths.get(p.substring(0, i));
 604                         try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.jar")) {
 605                             for (Path entry : stream) {
 606                                 paths.add(entry);
 607                             }
 608                         } catch (IOException e) {
 609                             throw new UncheckedIOException(e);
 610                         }
 611                     } else {
 612                         paths.add(Paths.get(p));
 613                     }
 614                 }
 615             }
 616             return paths;
 617         }
 618     }
 619 
 620 }