1 /*
   2  * Copyright (c) 2015, 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 static com.sun.tools.jdeps.Module.*;
  28 import static com.sun.tools.jdeps.Analyzer.NOT_FOUND;
  29 import static java.util.stream.Collectors.*;
  30 
  31 import com.sun.tools.classfile.AccessFlags;
  32 import com.sun.tools.classfile.ClassFile;
  33 import com.sun.tools.classfile.ConstantPoolException;
  34 import com.sun.tools.classfile.Dependencies;
  35 import com.sun.tools.classfile.Dependencies.ClassFileError;
  36 import com.sun.tools.classfile.Dependency;
  37 import com.sun.tools.classfile.Dependency.Location;
  38 
  39 import java.io.IOException;
  40 import java.io.UncheckedIOException;
  41 import java.util.Collections;
  42 import java.util.Deque;
  43 import java.util.HashMap;
  44 import java.util.HashSet;
  45 import java.util.Map;
  46 import java.util.Optional;
  47 import java.util.Set;
  48 import java.util.concurrent.Callable;
  49 import java.util.concurrent.ConcurrentHashMap;
  50 import java.util.concurrent.ConcurrentLinkedDeque;
  51 import java.util.concurrent.ExecutionException;
  52 import java.util.concurrent.ExecutorService;
  53 import java.util.concurrent.Executors;
  54 import java.util.concurrent.FutureTask;
  55 import java.util.stream.Stream;
  56 
  57 /**
  58  * Parses class files and finds dependences
  59  */
  60 class DependencyFinder {
  61     private static Finder API_FINDER = new Finder(true);
  62     private static Finder CLASS_FINDER = new Finder(false);
  63 
  64     private final JdepsConfiguration configuration;
  65     private final JdepsFilter filter;
  66 
  67     private final Map<Finder, Deque<Archive>> parsedArchives = new ConcurrentHashMap<>();
  68     private final Map<Location, Archive> parsedClasses = new ConcurrentHashMap<>();
  69 
  70     private final ExecutorService pool = Executors.newFixedThreadPool(2);
  71     private final Deque<FutureTask<Set<Location>>> tasks = new ConcurrentLinkedDeque<>();
  72 
  73     DependencyFinder(JdepsConfiguration configuration,
  74                      JdepsFilter filter) {
  75         this.configuration = configuration;
  76         this.filter = filter;
  77         this.parsedArchives.put(API_FINDER, new ConcurrentLinkedDeque<>());
  78         this.parsedArchives.put(CLASS_FINDER, new ConcurrentLinkedDeque<>());
  79     }
  80 
  81     Map<Location, Archive> locationToArchive() {
  82         return parsedClasses;
  83     }
  84 
  85     /**
  86      * Returns the modules of all dependencies found
  87      */
  88     Stream<Archive> getDependences(Archive source) {
  89         return source.getDependencies()
  90                      .map(this::locationToArchive)
  91                      .filter(a -> a != source);
  92     }
  93 
  94     /**
  95      * Returns the location to archive map; or NOT_FOUND.
  96      *
  97      * Location represents a parsed class.
  98      */
  99     Archive locationToArchive(Location location) {
 100         return parsedClasses.containsKey(location)
 101             ? parsedClasses.get(location)
 102             : configuration.findClass(location).orElse(NOT_FOUND);
 103     }
 104 
 105     /**
 106      * Returns a map from an archive to its required archives
 107      */
 108     Map<Archive, Set<Archive>> dependences() {
 109         Map<Archive, Set<Archive>> map = new HashMap<>();
 110         parsedArchives.values().stream()
 111             .flatMap(Deque::stream)
 112             .filter(a -> !a.isEmpty())
 113             .forEach(source -> {
 114                 Set<Archive> deps = getDependences(source).collect(toSet());
 115                 if (!deps.isEmpty()) {
 116                     map.put(source, deps);
 117                 }
 118         });
 119         return map;
 120     }
 121 
 122     boolean isParsed(Location location) {
 123         return parsedClasses.containsKey(location);
 124     }
 125 
 126     /**
 127      * Parses all class files from the given archive stream and returns
 128      * all target locations.
 129      */
 130     public Set<Location> parse(Stream<? extends Archive> archiveStream) {
 131         archiveStream.forEach(archive -> parse(archive, CLASS_FINDER));
 132         return waitForTasksCompleted();
 133     }
 134 
 135     /**
 136      * Parses the exported API class files from the given archive stream and
 137      * returns all target locations.
 138      */
 139     public Set<Location> parseExportedAPIs(Stream<? extends Archive> archiveStream) {
 140         archiveStream.forEach(archive -> parse(archive, API_FINDER));
 141         return waitForTasksCompleted();
 142     }
 143 
 144     /**
 145      * Parses the named class from the given archive and
 146      * returns all target locations the named class references.
 147      */
 148     public Set<Location> parse(Archive archive, String name) {
 149         try {
 150             return parse(archive, CLASS_FINDER, name);
 151         } catch (IOException e) {
 152             throw new UncheckedIOException(e);
 153         }
 154     }
 155 
 156     /**
 157      * Parses the exported API of the named class from the given archive and
 158      * returns all target locations the named class references.
 159      */
 160     public Set<Location> parseExportedAPIs(Archive archive, String name)
 161     {
 162         try {
 163             return parse(archive, API_FINDER, name);
 164         } catch (IOException e) {
 165             throw new UncheckedIOException(e);
 166         }
 167     }
 168 
 169     private Optional<FutureTask<Set<Location>>> parse(Archive archive, Finder finder) {
 170         if (parsedArchives.get(finder).contains(archive))
 171             return Optional.empty();
 172 
 173         parsedArchives.get(finder).add(archive);
 174 
 175         trace("parsing %s %s%n", archive.getName(), archive.path());
 176         FutureTask<Set<Location>> task = new FutureTask<>(() -> {
 177             Set<Location> targets = new HashSet<>();
 178             for (ClassFile cf : archive.reader().getClassFiles()) {
 179                 if (cf.access_flags.is(AccessFlags.ACC_MODULE))
 180                     continue;
 181 
 182                 String classFileName;
 183                 try {
 184                     classFileName = cf.getName();
 185                 } catch (ConstantPoolException e) {
 186                     throw new ClassFileError(e);
 187                 }
 188 
 189                 // filter source class/archive
 190                 String cn = classFileName.replace('/', '.');
 191                 if (!finder.accept(archive, cn, cf.access_flags))
 192                     continue;
 193 
 194                 // tests if this class matches the -include
 195                 if (!filter.matches(cn))
 196                     continue;
 197 
 198                 for (Dependency d : finder.findDependencies(cf)) {
 199                     if (filter.accepts(d)) {
 200                         archive.addClass(d.getOrigin(), d.getTarget());
 201                         targets.add(d.getTarget());
 202                     } else {
 203                         // ensure that the parsed class is added the archive
 204                         archive.addClass(d.getOrigin());
 205                     }
 206                     parsedClasses.putIfAbsent(d.getOrigin(), archive);
 207                 }
 208             }
 209 
 210             return targets;
 211         });
 212         tasks.add(task);
 213         pool.submit(task);
 214         return Optional.of(task);
 215     }
 216 
 217     private Set<Location> parse(Archive archive, Finder finder, String name)
 218         throws IOException
 219     {
 220         ClassFile cf = archive.reader().getClassFile(name);
 221         if (cf == null) {
 222             throw new IllegalArgumentException(archive.getName() +
 223                 " does not contain " + name);
 224         }
 225 
 226         if (cf.access_flags.is(AccessFlags.ACC_MODULE))
 227             return Collections.emptySet();
 228 
 229         Set<Location> targets = new HashSet<>();
 230         String cn;
 231         try {
 232             cn =  cf.getName().replace('/', '.');
 233         } catch (ConstantPoolException e) {
 234             throw new Dependencies.ClassFileError(e);
 235         }
 236 
 237         if (!finder.accept(archive, cn, cf.access_flags))
 238             return targets;
 239 
 240         // tests if this class matches the -include
 241         if (!filter.matches(cn))
 242             return targets;
 243 
 244         // skip checking filter.matches
 245         for (Dependency d : finder.findDependencies(cf)) {
 246             if (filter.accepts(d)) {
 247                 targets.add(d.getTarget());
 248                 archive.addClass(d.getOrigin(), d.getTarget());
 249             } else {
 250                 // ensure that the parsed class is added the archive
 251                 archive.addClass(d.getOrigin());
 252             }
 253             parsedClasses.putIfAbsent(d.getOrigin(), archive);
 254         }
 255         return targets;
 256     }
 257 
 258     /*
 259      * Waits until all submitted tasks are completed.
 260      */
 261     private Set<Location> waitForTasksCompleted() {
 262         try {
 263             Set<Location> targets = new HashSet<>();
 264             FutureTask<Set<Location>> task;
 265             while ((task = tasks.poll()) != null) {
 266                 // wait for completion
 267                 if (!task.isDone())
 268                     targets.addAll(task.get());
 269             }
 270             return targets;
 271         } catch (InterruptedException|ExecutionException e) {
 272             throw new Error(e);
 273         }
 274     }
 275 
 276     /*
 277      * Shutdown the executor service.
 278      */
 279     void shutdown() {
 280         pool.shutdown();
 281     }
 282 
 283     private interface SourceFilter {
 284         boolean accept(Archive archive, String cn, AccessFlags accessFlags);
 285     }
 286 
 287     private static class Finder implements Dependency.Finder, SourceFilter {
 288         private final Dependency.Finder finder;
 289         private final boolean apiOnly;
 290         Finder(boolean apiOnly) {
 291             this.apiOnly = apiOnly;
 292             this.finder = apiOnly
 293                 ? Dependencies.getAPIFinder(AccessFlags.ACC_PROTECTED)
 294                 : Dependencies.getClassDependencyFinder();
 295 
 296         }
 297 
 298         @Override
 299         public boolean accept(Archive archive, String cn, AccessFlags accessFlags) {
 300             int i = cn.lastIndexOf('.');
 301             String pn = i > 0 ? cn.substring(0, i) : "";
 302 
 303             // if -apionly is specified, analyze only exported and public types
 304             // All packages are exported in unnamed module.
 305             return apiOnly ? archive.getModule().isExported(pn) &&
 306                                  accessFlags.is(AccessFlags.ACC_PUBLIC)
 307                            : true;
 308         }
 309 
 310         @Override
 311         public Iterable<? extends Dependency> findDependencies(ClassFile classfile) {
 312             return finder.findDependencies(classfile);
 313         }
 314     }
 315 }