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 package com.sun.tools.jdeps;
  26 
  27 import java.io.PrintStream;
  28 import java.util.Comparator;
  29 import java.util.HashMap;
  30 import java.util.HashSet;
  31 import java.util.List;
  32 import java.util.Map;
  33 import java.util.Objects;
  34 import java.util.Set;
  35 import java.util.stream.Collectors;
  36 import java.util.stream.Stream;
  37 
  38 import com.sun.tools.classfile.Dependency.Location;
  39 
  40 /**
  41  * Dependency Analyzer.
  42  */
  43 public class Analyzer {
  44     /**
  45      * Type of the dependency analysis.  Appropriate level of data
  46      * will be stored.
  47      */
  48     public enum Type {
  49         SUMMARY,
  50         PACKAGE,
  51         CLASS,
  52         VERBOSE
  53     }
  54 
  55     /**
  56      * Filter to be applied when analyzing the dependencies from the given archives.
  57      * Only the accepted dependencies are recorded.
  58      */
  59     interface Filter {
  60         boolean accepts(Location origin, Archive originArchive, Location target, Archive targetArchive);
  61     }
  62 
  63     protected final Type type;
  64     protected final Filter filter;
  65     protected final Map<Archive, ArchiveDeps> results = new HashMap<>();
  66     protected final Map<Location, Archive> map = new HashMap<>();
  67     private static final Archive NOT_FOUND
  68         = new Archive(JdepsTask.getMessage("artifact.not.found"));
  69 
  70     /**
  71      * Constructs an Analyzer instance.
  72      *
  73      * @param type Type of the dependency analysis
  74      * @param filter
  75      */
  76     public Analyzer(Type type, Filter filter) {
  77         this.type = type;
  78         this.filter = filter;
  79     }
  80 
  81     /**
  82      * Performs the dependency analysis on the given archives.
  83      */
  84     public boolean run(List<Archive> archives) {
  85         // build a map from Location to Archive
  86         buildLocationArchiveMap(archives);
  87 
  88         // traverse and analyze all dependencies
  89         for (Archive archive : archives) {
  90             ArchiveDeps deps = new ArchiveDeps(archive, type);
  91             archive.visitDependences(deps);
  92             results.put(archive, deps);
  93         }
  94         return true;
  95     }
  96 
  97     protected void buildLocationArchiveMap(List<Archive> archives) {
  98         // build a map from Location to Archive
  99         for (Archive archive: archives) {
 100             for (Location l: archive.getClasses()) {
 101                 if (!map.containsKey(l)) {
 102                     map.put(l, archive);
 103                 } else {
 104                     // duplicated class warning?
 105                 }
 106             }
 107         }
 108     }
 109 
 110     public boolean hasDependences(Archive archive) {
 111         if (results.containsKey(archive)) {
 112             return results.get(archive).dependencies().size() > 0;
 113         }
 114         return false;
 115     }
 116 
 117     public Set<String> dependences(Archive source) {
 118         ArchiveDeps result = results.get(source);
 119         return result.dependencies().stream()
 120                      .map(Dep::target)
 121                      .collect(Collectors.toSet());
 122     }
 123 
 124     public interface Visitor {
 125         /**
 126          * Visits a recorded dependency from origin to target which can be
 127          * a fully-qualified classname, a package name, a module or
 128          * archive name depending on the Analyzer's type.
 129          */
 130         public void visitDependence(String origin, Archive originArchive,
 131                                     String target, Archive targetArchive);
 132     }
 133 
 134     /**
 135      * Visit the dependencies of the given source.
 136      * If the requested level is SUMMARY, it will visit the required archives list.
 137      */
 138     public void visitDependences(Archive source, Visitor v, Type level) {
 139         if (level == Type.SUMMARY) {
 140             final ArchiveDeps result = results.get(source);
 141             final Set<Archive> reqs = result.requires();
 142             Stream<Archive> stream = reqs.stream();
 143             if (reqs.isEmpty()) {
 144                 if (hasDependences(source)) {
 145                     // If reqs.isEmpty() and we have dependences, then it means
 146                     // that the dependences are from 'source' onto itself.
 147                     stream = Stream.of(source);
 148                 }
 149             }
 150             stream.sorted(Comparator.comparing(Archive::getName))
 151                   .forEach(archive -> {
 152                       Profile profile = result.getTargetProfile(archive);
 153                       v.visitDependence(source.getName(), source,
 154                                         profile != null ? profile.profileName() : archive.getName(), archive);
 155                   });
 156         } else {
 157             ArchiveDeps result = results.get(source);
 158             if (level != type) {
 159                 // requesting different level of analysis
 160                 result = new ArchiveDeps(source, level);
 161                 source.visitDependences(result);
 162             }
 163             result.dependencies().stream()
 164                   .sorted(Comparator.comparing(Dep::origin)
 165                                     .thenComparing(Dep::target))
 166                   .forEach(d -> v.visitDependence(d.origin(), d.originArchive(), d.target(), d.targetArchive()));
 167         }
 168     }
 169 
 170     public void visitDependences(Archive source, Visitor v) {
 171         visitDependences(source, v, type);
 172     }
 173 
 174     /**
 175      * ArchiveDeps contains the dependencies for an Archive that can have one or
 176      * more classes.
 177      */
 178     class ArchiveDeps implements Archive.Visitor {
 179         protected final Archive archive;
 180         protected final Set<Archive> requires;
 181         protected final Set<Dep> deps;
 182         protected final Type level;
 183         private Profile profile;
 184         ArchiveDeps(Archive archive, Type level) {
 185             this.archive = archive;
 186             this.deps = new HashSet<>();
 187             this.requires = new HashSet<>();
 188             this.level = level;
 189         }
 190 
 191         Set<Dep> dependencies() {
 192             return deps;
 193         }
 194 
 195         Set<Archive> requires() {
 196             return requires;
 197         }
 198 
 199         Profile getTargetProfile(Archive target) {
 200             if (target instanceof Module) {
 201                 return Profile.getProfile((Module) target);
 202             } else {
 203                 return null;
 204             }
 205         }
 206 
 207         Archive findArchive(Location t) {
 208             Archive target = archive.getClasses().contains(t) ? archive : map.get(t);
 209             if (target == null) {
 210                 map.put(t, target = NOT_FOUND);
 211             }
 212             return target;
 213         }
 214 
 215         // return classname or package name depedning on the level
 216         private String getLocationName(Location o) {
 217             if (level == Type.CLASS || level == Type.VERBOSE) {
 218                 return o.getClassName();
 219             } else {
 220                 String pkg = o.getPackageName();
 221                 return pkg.isEmpty() ? "<unnamed>" : pkg;
 222             }
 223         }
 224 
 225         @Override
 226         public void visit(Location o, Location t) {
 227             Archive targetArchive = findArchive(t);
 228             if (filter.accepts(o, archive, t, targetArchive)) {
 229                 addDep(o, t);
 230                 if (archive != targetArchive && !requires.contains(targetArchive)) {
 231                     requires.add(targetArchive);
 232                 }
 233             }
 234             if (targetArchive instanceof Module) {
 235                 Profile p = Profile.getProfile(t.getPackageName());
 236                 if (profile == null || (p != null && p.compareTo(profile) > 0)) {
 237                     profile = p;
 238                 }
 239             }
 240         }
 241 
 242         private Dep curDep;
 243         protected Dep addDep(Location o, Location t) {
 244             String origin = getLocationName(o);
 245             String target = getLocationName(t);
 246             Archive targetArchive = findArchive(t);
 247             if (curDep != null &&
 248                     curDep.origin().equals(origin) &&
 249                     curDep.originArchive() == archive &&
 250                     curDep.target().equals(target) &&
 251                     curDep.targetArchive() == targetArchive) {
 252                 return curDep;
 253             }
 254 
 255             Dep e = new Dep(origin, archive, target, targetArchive);
 256             if (deps.contains(e)) {
 257                 for (Dep e1 : deps) {
 258                     if (e.equals(e1)) {
 259                         curDep = e1;
 260                     }
 261                 }
 262             } else {
 263                 deps.add(e);
 264                 curDep = e;
 265             }
 266             return curDep;
 267         }
 268     }
 269 
 270     /*
 271      * Class-level or package-level dependency
 272      */
 273     class Dep {
 274         final String origin;
 275         final Archive originArchive;
 276         final String target;
 277         final Archive targetArchive;
 278 
 279         Dep(String origin, Archive originArchive, String target, Archive targetArchive) {
 280             this.origin = origin;
 281             this.originArchive = originArchive;
 282             this.target = target;
 283             this.targetArchive = targetArchive;
 284         }
 285 
 286         String origin() {
 287             return origin;
 288         }
 289 
 290         Archive originArchive() {
 291             return originArchive;
 292         }
 293 
 294         String target() {
 295             return target;
 296         }
 297 
 298         Archive targetArchive() {
 299             return targetArchive;
 300         }
 301 
 302         @Override
 303         @SuppressWarnings("unchecked")
 304         public boolean equals(Object o) {
 305             if (o instanceof Dep) {
 306                 Dep d = (Dep) o;
 307                 return this.origin.equals(d.origin) &&
 308                         this.originArchive == d.originArchive &&
 309                         this.target.equals(d.target) &&
 310                         this.targetArchive == d.targetArchive;
 311             }
 312             return false;
 313         }
 314 
 315         @Override
 316         public int hashCode() {
 317             int hash = 7;
 318             hash = 67*hash + Objects.hashCode(this.origin)
 319                            + Objects.hashCode(this.originArchive)
 320                            + Objects.hashCode(this.target)
 321                            + Objects.hashCode(this.targetArchive);
 322             return hash;
 323         }
 324 
 325         public String toString() {
 326             return String.format("%s (%s) -> %s (%s)%n",
 327                     origin, originArchive.getName(),
 328                     target, targetArchive.getName());
 329         }
 330     }
 331 
 332     static Analyzer getExportedAPIsAnalyzer() {
 333         return new ModuleAccessAnalyzer(ModuleAccessAnalyzer.reexportsFilter, true);
 334     }
 335 
 336     static Analyzer getModuleAccessAnalyzer() {
 337         return new ModuleAccessAnalyzer(ModuleAccessAnalyzer.accessCheckFilter, false);
 338     }
 339 
 340     private static class ModuleAccessAnalyzer extends Analyzer {
 341         private final boolean apionly;
 342         ModuleAccessAnalyzer(Filter filter, boolean apionly) {
 343             super(Type.VERBOSE, filter);
 344             this.apionly = apionly;
 345         }
 346         /**
 347          * Verify module access
 348          */
 349         public boolean run(List<Archive> archives) {
 350             // build a map from Location to Archive
 351             buildLocationArchiveMap(archives);
 352 
 353             // traverse and analyze all dependencies
 354             int count = 0;
 355             for (Archive archive : archives) {
 356                 ArchiveDeps checker = new ArchiveDeps(archive, type);
 357                 archive.visitDependences(checker);
 358                 count += checker.dependencies().size();
 359                 // output if any error
 360                 Module m = (Module)archive;
 361                 printDependences(System.err, m, checker.dependencies());
 362                 results.put(archive, checker);
 363             }
 364             return count == 0;
 365         }
 366 
 367         private void printDependences(PrintStream out, Module m, Set<Dep> deps) {
 368             if (deps.isEmpty())
 369                 return;
 370 
 371             String msg = apionly ? "API reference:" : "inaccessible reference:";
 372             deps.stream().sorted(Comparator.comparing(Dep::origin)
 373                                            .thenComparing(Dep::target))
 374                 .forEach(d -> out.format("%s %s (%s) -> %s (%s)%n", msg,
 375                                          d.origin(), d.originArchive().getName(),
 376                                          d.target(), d.targetArchive().getName()));
 377             if (apionly) {
 378                 out.format("Dependences missing re-exports=\"true\" attribute:%n");
 379                 deps.stream()
 380                         .map(Dep::targetArchive)
 381                         .map(Archive::getName)
 382                         .distinct()
 383                         .sorted()
 384                         .forEach(d -> out.format("  %s -> %s%n", m.name(), d));
 385             }
 386         }
 387 
 388         private static Module findModule(Archive archive) {
 389             if (Module.class.isInstance(archive)) {
 390                 return (Module) archive;
 391             } else {
 392                 return null;
 393             }
 394         }
 395 
 396         // returns true if target is accessible by origin
 397         private static boolean canAccess(Location o, Archive originArchive, Location t, Archive targetArchive) {
 398             Module origin = findModule(originArchive);
 399             Module target = findModule(targetArchive);
 400 
 401             if (targetArchive == Analyzer.NOT_FOUND) {
 402                 return false;
 403             }
 404 
 405             // unnamed module
 406             // ## should check public type?
 407             if (target == null)
 408                 return true;
 409 
 410             // module-private
 411             if (origin == target)
 412                 return true;
 413 
 414             return target.isAccessibleTo(t.getClassName(), origin);
 415         }
 416 
 417         static final Filter accessCheckFilter = new Filter() {
 418             @Override
 419             public boolean accepts(Location o, Archive originArchive, Location t, Archive targetArchive) {
 420                 return !canAccess(o, originArchive, t, targetArchive);
 421             }
 422         };
 423 
 424         static final Filter reexportsFilter = new Filter() {
 425             @Override
 426             public boolean accepts(Location o, Archive originArchive, Location t, Archive targetArchive) {
 427                 Module origin = findModule(originArchive);
 428                 Module target = findModule(targetArchive);
 429                 if (!origin.isExportedPackage(o.getPackageName())) {
 430                     // filter non-exported classes
 431                     return false;
 432                 }
 433 
 434                 boolean accessible = canAccess(o, originArchive, t, targetArchive);
 435                 if (!accessible)
 436                     return true;
 437 
 438                 String mn = target.name();
 439                 // skip checking re-exports for java.base
 440                 if (origin == target || "java.base".equals(mn))
 441                     return false;
 442 
 443                 assert origin.requires().containsKey(mn);  // otherwise, should not be accessible
 444                 if (origin.requires().get(mn)) {
 445                     return false;
 446                 }
 447                 return true;
 448             }
 449         };
 450     }
 451 }