1 /*
   2  * Copyright (c) 2013, 2014, 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 com.sun.tools.classfile.Dependency.Location;
  29 
  30 import java.io.BufferedReader;
  31 import java.io.IOException;
  32 import java.io.InputStream;
  33 import java.io.InputStreamReader;
  34 import java.io.UncheckedIOException;
  35 import java.lang.module.ModuleDescriptor;
  36 import java.util.Collections;
  37 import java.util.Comparator;
  38 import java.util.HashMap;
  39 import java.util.HashSet;
  40 import java.util.Map;
  41 import java.util.Objects;
  42 import java.util.Set;
  43 import java.util.function.Predicate;
  44 import java.util.stream.Collectors;
  45 import java.util.stream.Stream;
  46 
  47 /**
  48  * Dependency Analyzer.
  49  */
  50 public class Analyzer {
  51     /**
  52      * Type of the dependency analysis.  Appropriate level of data
  53      * will be stored.
  54      */
  55     public enum Type {
  56         SUMMARY,
  57         MODULE,  // equivalent to summary in addition, print module descriptor
  58         PACKAGE,
  59         CLASS,
  60         VERBOSE
  61     }
  62 
  63     /**
  64      * Filter to be applied when analyzing the dependencies from the given archives.
  65      * Only the accepted dependencies are recorded.
  66      */
  67     interface Filter {
  68         boolean accepts(Location origin, Archive originArchive,
  69                         Location target, Archive targetArchive);
  70     }
  71 
  72     protected final JdepsConfiguration configuration;
  73     protected final Type type;
  74     protected final Filter filter;
  75     protected final Map<Archive, Dependences> results = new HashMap<>();
  76     protected final Map<Location, Archive> locationToArchive = new HashMap<>();
  77     static final Archive NOT_FOUND
  78         = new Archive(JdepsTask.getMessage("artifact.not.found"));
  79     static final Predicate<Archive> ANY = a -> true;
  80 
  81     /**
  82      * Constructs an Analyzer instance.
  83      *
  84      * @param type Type of the dependency analysis
  85      * @param filter
  86      */
  87     Analyzer(JdepsConfiguration config, Type type, Filter filter) {
  88         this.configuration = config;
  89         this.type = type;
  90         this.filter = filter;
  91     }
  92 
  93     /**
  94      * Performs the dependency analysis on the given archives.
  95      */
  96     boolean run(Iterable<? extends Archive> archives,
  97                 Map<Location, Archive> locationMap)
  98     {
  99         this.locationToArchive.putAll(locationMap);
 100 
 101         // traverse and analyze all dependencies
 102         for (Archive archive : archives) {
 103             Dependences deps = new Dependences(archive, type);
 104             archive.visitDependences(deps);
 105             results.put(archive, deps);
 106         }
 107         return true;
 108     }
 109 
 110     /**
 111      * Returns the analyzed archives
 112      */
 113     Set<Archive> archives() {
 114         return results.keySet();
 115     }
 116 
 117     /**
 118      * Returns true if the given archive has dependences.
 119      */
 120     boolean hasDependences(Archive archive) {
 121         if (results.containsKey(archive)) {
 122             return results.get(archive).dependencies().size() > 0;
 123         }
 124         return false;
 125     }
 126 
 127     /**
 128      * Returns the dependences, either class name or package name
 129      * as specified in the given verbose level, from the given source.
 130      */
 131     Set<String> dependences(Archive source) {
 132         if (!results.containsKey(source)) {
 133             return Collections.emptySet();
 134         }
 135 
 136         return results.get(source).dependencies()
 137                       .stream()
 138                       .map(Dep::target)
 139                       .collect(Collectors.toSet());
 140     }
 141 
 142     /**
 143      * Returns the direct dependences of the given source
 144      */
 145     Stream<Archive> requires(Archive source) {
 146         if (!results.containsKey(source)) {
 147             return Stream.empty();
 148         }
 149         return results.get(source).requires()
 150                       .stream();
 151     }
 152 
 153     interface Visitor {
 154         /**
 155          * Visits a recorded dependency from origin to target which can be
 156          * a fully-qualified classname, a package name, a module or
 157          * archive name depending on the Analyzer's type.
 158          */
 159         public void visitDependence(String origin, Archive originArchive,
 160                                     String target, Archive targetArchive);
 161     }
 162 
 163     /**
 164      * Visit the dependencies of the given source.
 165      * If the requested level is SUMMARY, it will visit the required archives list.
 166      */
 167     void visitDependences(Archive source, Visitor v, Type level, Predicate<Archive> targetFilter) {
 168         if (level == Type.SUMMARY) {
 169             final Dependences result = results.get(source);
 170             final Set<Archive> reqs = result.requires();
 171             Stream<Archive> stream = reqs.stream();
 172             if (reqs.isEmpty()) {
 173                 if (hasDependences(source)) {
 174                     // If reqs.isEmpty() and we have dependences, then it means
 175                     // that the dependences are from 'source' onto itself.
 176                     stream = Stream.of(source);
 177                 }
 178             }
 179             stream.sorted(Comparator.comparing(Archive::getName))
 180                   .forEach(archive -> {
 181                       Profile profile = result.getTargetProfile(archive);
 182                       v.visitDependence(source.getName(), source,
 183                                         profile != null ? profile.profileName()
 184                                                         : archive.getName(), archive);
 185                   });
 186         } else {
 187             Dependences result = results.get(source);
 188             if (level != type) {
 189                 // requesting different level of analysis
 190                 result = new Dependences(source, level, targetFilter);
 191                 source.visitDependences(result);
 192             }
 193             result.dependencies().stream()
 194                   .sorted(Comparator.comparing(Dep::origin)
 195                                     .thenComparing(Dep::target))
 196                   .forEach(d -> v.visitDependence(d.origin(), d.originArchive(),
 197                                                   d.target(), d.targetArchive()));
 198         }
 199     }
 200 
 201     void visitDependences(Archive source, Visitor v) {
 202         visitDependences(source, v, type, ANY);
 203     }
 204 
 205     void visitDependences(Archive source, Visitor v, Type level) {
 206         visitDependences(source, v, level, ANY);
 207     }
 208 
 209     /**
 210      * Dependences contains the dependencies for an Archive that can have one or
 211      * more classes.
 212      */
 213     class Dependences implements Archive.Visitor {
 214         protected final Archive archive;
 215         protected final Set<Archive> requires;
 216         protected final Set<Dep> deps;
 217         protected final Type level;
 218         protected final Predicate<Archive> targetFilter;
 219         private Profile profile;
 220         Dependences(Archive archive, Type level) {
 221             this(archive, level, ANY);
 222         }
 223         Dependences(Archive archive, Type level, Predicate<Archive> targetFilter) {
 224             this.archive = archive;
 225             this.deps = new HashSet<>();
 226             this.requires = new HashSet<>();
 227             this.level = level;
 228             this.targetFilter = targetFilter;
 229         }
 230 
 231         Set<Dep> dependencies() {
 232             return deps;
 233         }
 234 
 235         Set<Archive> requires() {
 236             return requires;
 237         }
 238 
 239         Profile getTargetProfile(Archive target) {
 240             if (target.getModule().isJDK()) {
 241                 return Profile.getProfile((Module) target);
 242             } else {
 243                 return null;
 244             }
 245         }
 246 
 247         /*
 248          * Returns the archive that contains the given location.
 249          */
 250         Archive findArchive(Location t) {
 251             // local in this archive
 252             if (archive.getClasses().contains(t))
 253                 return archive;
 254 
 255             Archive target;
 256             if (locationToArchive.containsKey(t)) {
 257                 target = locationToArchive.get(t);
 258             } else {
 259                 // special case JDK removed API
 260                 target = configuration.findClass(t)
 261                     .orElseGet(() -> REMOVED_JDK_INTERNALS.contains(t)
 262                                         ? REMOVED_JDK_INTERNALS
 263                                         : NOT_FOUND);
 264             }
 265             return locationToArchive.computeIfAbsent(t, _k -> target);
 266         }
 267 
 268         // return classname or package name depending on the level
 269         private String getLocationName(Location o) {
 270             if (level == Type.CLASS || level == Type.VERBOSE) {
 271                 return VersionHelper.get(o.getClassName());
 272             } else {
 273                 String pkg = o.getPackageName();
 274                 return pkg.isEmpty() ? "<unnamed>" : pkg;
 275             }
 276         }
 277 
 278         @Override
 279         public void visit(Location o, Location t) {
 280             Archive targetArchive = findArchive(t);
 281             if (filter.accepts(o, archive, t, targetArchive) && targetFilter.test(targetArchive)) {
 282                 addDep(o, t);
 283                 if (archive != targetArchive && !requires.contains(targetArchive)) {
 284                     requires.add(targetArchive);
 285                 }
 286             }
 287             if (targetArchive.getModule().isNamed()) {
 288                 Profile p = Profile.getProfile(t.getPackageName());
 289                 if (profile == null || (p != null && p.compareTo(profile) > 0)) {
 290                     profile = p;
 291                 }
 292             }
 293         }
 294 
 295         private Dep curDep;
 296         protected Dep addDep(Location o, Location t) {
 297             String origin = getLocationName(o);
 298             String target = getLocationName(t);
 299             Archive targetArchive = findArchive(t);
 300             if (curDep != null &&
 301                     curDep.origin().equals(origin) &&
 302                     curDep.originArchive() == archive &&
 303                     curDep.target().equals(target) &&
 304                     curDep.targetArchive() == targetArchive) {
 305                 return curDep;
 306             }
 307 
 308             Dep e = new Dep(origin, archive, target, targetArchive);
 309             if (deps.contains(e)) {
 310                 for (Dep e1 : deps) {
 311                     if (e.equals(e1)) {
 312                         curDep = e1;
 313                     }
 314                 }
 315             } else {
 316                 deps.add(e);
 317                 curDep = e;
 318             }
 319             return curDep;
 320         }
 321     }
 322 
 323     /*
 324      * Class-level or package-level dependency
 325      */
 326     class Dep {
 327         final String origin;
 328         final Archive originArchive;
 329         final String target;
 330         final Archive targetArchive;
 331 
 332         Dep(String origin, Archive originArchive, String target, Archive targetArchive) {
 333             this.origin = origin;
 334             this.originArchive = originArchive;
 335             this.target = target;
 336             this.targetArchive = targetArchive;
 337         }
 338 
 339         String origin() {
 340             return origin;
 341         }
 342 
 343         Archive originArchive() {
 344             return originArchive;
 345         }
 346 
 347         String target() {
 348             return target;
 349         }
 350 
 351         Archive targetArchive() {
 352             return targetArchive;
 353         }
 354 
 355         @Override
 356         @SuppressWarnings("unchecked")
 357         public boolean equals(Object o) {
 358             if (o instanceof Dep) {
 359                 Dep d = (Dep) o;
 360                 return this.origin.equals(d.origin) &&
 361                         this.originArchive == d.originArchive &&
 362                         this.target.equals(d.target) &&
 363                         this.targetArchive == d.targetArchive;
 364             }
 365             return false;
 366         }
 367 
 368         @Override
 369         public int hashCode() {
 370             return Objects.hash(this.origin,
 371                                 this.originArchive,
 372                                 this.target,
 373                                 this.targetArchive);
 374         }
 375 
 376         public String toString() {
 377             return String.format("%s (%s) -> %s (%s)%n",
 378                     origin, originArchive.getName(),
 379                     target, targetArchive.getName());
 380         }
 381     }
 382 
 383     /*
 384      * Returns true if the given archive represents not found.
 385      */
 386     static boolean notFound(Archive archive) {
 387         return archive == NOT_FOUND || archive == REMOVED_JDK_INTERNALS;
 388     }
 389 
 390     static final Jdk8Internals REMOVED_JDK_INTERNALS = new Jdk8Internals();
 391 
 392     static class Jdk8Internals extends Module {
 393         private static final String NAME = "JDK removed internal API";
 394         private static final String JDK8_INTERNALS = "/com/sun/tools/jdeps/resources/jdk8_internals.txt";
 395         private final Set<String> jdk8Internals;
 396         private Jdk8Internals() {
 397             super(NAME, ModuleDescriptor.newModule("jdk8internals").build(), true);
 398             try (InputStream in = JdepsTask.class.getResourceAsStream(JDK8_INTERNALS);
 399                  BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
 400                 this.jdk8Internals = reader.lines()
 401                                           .filter(ln -> !ln.startsWith("#"))
 402                                           .collect(Collectors.toSet());
 403             } catch (IOException e) {
 404                 throw new UncheckedIOException(e);
 405             }
 406         }
 407 
 408         public boolean contains(Location location) {
 409             String cn = location.getClassName();
 410             int i = cn.lastIndexOf('.');
 411             String pn = i > 0 ? cn.substring(0, i) : "";
 412 
 413             return jdk8Internals.contains(pn);
 414         }
 415 
 416         @Override
 417         public boolean isJDK() {
 418             return true;
 419         }
 420 
 421         @Override
 422         public boolean isExported(String pn) {
 423             return false;
 424         }
 425     }
 426 }