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