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