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.nio.file.Paths;
  42 import java.util.Collections;
  43 import java.util.Deque;
  44 import java.util.HashMap;
  45 import java.util.HashSet;
  46 import java.util.Map;
  47 import java.util.Optional;
  48 import java.util.Set;
  49 import java.util.concurrent.Callable;
  50 import java.util.concurrent.ConcurrentHashMap;
  51 import java.util.concurrent.ConcurrentLinkedDeque;
  52 import java.util.concurrent.ExecutionException;
  53 import java.util.concurrent.ExecutorService;
  54 import java.util.concurrent.Executors;
  55 import java.util.concurrent.FutureTask;
  56 import java.util.stream.Stream;
  57 
  58 /**
  59  * Parses class files and finds dependences
  60  */
  61 class DependencyFinder {
  62     private static Finder API_FINDER = new Finder(true);
  63     private static Finder CLASS_FINDER = new Finder(false);
  64 
  65     private final JdepsConfiguration configuration;
  66     private final JdepsFilter filter;
  67 
  68     private final Map<Finder, Deque<Archive>> parsedArchives = new ConcurrentHashMap<>();
  69     private final Map<Location, Archive> parsedClasses = new ConcurrentHashMap<>();
  70 
  71     private final ExecutorService pool = Executors.newFixedThreadPool(2);
  72     private final Deque<FutureTask<Set<Location>>> tasks = new ConcurrentLinkedDeque<>();
  73 
  74     DependencyFinder(JdepsConfiguration configuration,
  75                      JdepsFilter filter) {
  76         this.configuration = configuration;
  77         this.filter = filter;
  78         this.parsedArchives.put(API_FINDER, new ConcurrentLinkedDeque<>());
  79         this.parsedArchives.put(CLASS_FINDER, new ConcurrentLinkedDeque<>());
  80     }
  81 
  82     Map<Location, Archive> locationToArchive() {
  83         return parsedClasses;
  84     }
  85 
  86     /**
  87      * Returns the modules of all dependencies found
  88      */
  89     Stream<Archive> getDependences(Archive source) {
  90         return source.getDependencies()
  91                      .map(this::locationToArchive)
  92                      .filter(a -> a != source);
  93     }
  94 
  95     /**
  96      * Returns the location to archive map; or NOT_FOUND.
  97      *
  98      * Location represents a parsed class.
  99      */
 100     Archive locationToArchive(Location location) {
 101         return parsedClasses.containsKey(location)
 102             ? parsedClasses.get(location)
 103             : configuration.findClass(location).orElse(NOT_FOUND);
 104     }
 105 
 106     /**
 107      * Returns a map from an archive to its required archives
 108      */
 109     Map<Archive, Set<Archive>> dependences() {
 110         Map<Archive, Set<Archive>> map = new HashMap<>();
 111         parsedArchives.values().stream()
 112             .flatMap(Deque::stream)
 113             .filter(a -> !a.isEmpty())
 114             .forEach(source -> {
 115                 Set<Archive> deps = getDependences(source).collect(toSet());
 116                 if (!deps.isEmpty()) {
 117                     map.put(source, deps);
 118                 }
 119         });
 120         return map;
 121     }
 122 
 123     boolean isParsed(Location location) {
 124         return parsedClasses.containsKey(location);
 125     }
 126 
 127     /**
 128      * Parses all class files from the given archive stream and returns
 129      * all target locations.
 130      */
 131     public Set<Location> parse(Stream<? extends Archive> archiveStream) {
 132         archiveStream.forEach(archive -> parse(archive, CLASS_FINDER));
 133         return waitForTasksCompleted();
 134     }
 135 
 136     /**
 137      * Parses the exported API class files from the given archive stream and
 138      * returns all target locations.
 139      */
 140     public Set<Location> parseExportedAPIs(Stream<? extends Archive> archiveStream) {
 141         archiveStream.forEach(archive -> parse(archive, API_FINDER));
 142         return waitForTasksCompleted();
 143     }
 144 
 145     /**
 146      * Parses the named class from the given archive and
 147      * returns all target locations the named class references.
 148      */
 149     public Set<Location> parse(Archive archive, String name) {
 150         try {
 151             return parse(archive, CLASS_FINDER, name);
 152         } catch (IOException e) {
 153             throw new UncheckedIOException(e);
 154         }
 155     }
 156 
 157     /**
 158      * Parses the exported API of the named class from the given archive and
 159      * returns all target locations the named class references.
 160      */
 161     public Set<Location> parseExportedAPIs(Archive archive, String name)
 162     {
 163         try {
 164             return parse(archive, API_FINDER, name);
 165         } catch (IOException e) {
 166             throw new UncheckedIOException(e);
 167         }
 168     }
 169 
 170     private Optional<FutureTask<Set<Location>>> parse(Archive archive, Finder finder) {
 171         if (parsedArchives.get(finder).contains(archive))
 172             return Optional.empty();
 173 
 174         parsedArchives.get(finder).add(archive);
 175 
 176         trace("parsing %s %s%n", archive.getName(), archive.getPathName());
 177         FutureTask<Set<Location>> task = new FutureTask<>(() -> {
 178             Set<Location> targets = new HashSet<>();
 179             for (ClassFile cf : archive.reader().getClassFiles()) {
 180                 if (cf.access_flags.is(AccessFlags.ACC_MODULE))
 181                     continue;
 182 
 183                 String classFileName;
 184                 try {
 185                     classFileName = cf.getName();
 186                 } catch (ConstantPoolException e) {
 187                     throw new ClassFileError(e);
 188                 }
 189 
 190                 // filter source class/archive
 191                 String cn = classFileName.replace('/', '.');
 192                 if (!finder.accept(archive, cn, cf.access_flags))
 193                     continue;
 194 
 195                 // tests if this class matches the -include
 196                 if (!filter.matches(cn))
 197                     continue;
 198 
 199                 for (Dependency d : finder.findDependencies(cf)) {
 200                     if (filter.accepts(d)) {
 201                         archive.addClass(d.getOrigin(), d.getTarget());
 202                         targets.add(d.getTarget());
 203                     } else {
 204                         // ensure that the parsed class is added the archive
 205                         archive.addClass(d.getOrigin());
 206                     }
 207                     parsedClasses.putIfAbsent(d.getOrigin(), archive);
 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                 targets.addAll(task.get());
 268             }
 269             return targets;
 270         } catch (InterruptedException|ExecutionException e) {
 271             throw new Error(e);
 272         }
 273     }
 274 
 275     /*
 276      * Shutdown the executor service.
 277      */
 278     void shutdown() {
 279         pool.shutdown();
 280     }
 281 
 282     private interface SourceFilter {
 283         boolean accept(Archive archive, String cn, AccessFlags accessFlags);
 284     }
 285 
 286     private static class Finder implements Dependency.Finder, SourceFilter {
 287         private final Dependency.Finder finder;
 288         private final boolean apiOnly;
 289         Finder(boolean apiOnly) {
 290             this.apiOnly = apiOnly;
 291             this.finder = apiOnly
 292                 ? Dependencies.getAPIFinder(AccessFlags.ACC_PROTECTED)
 293                 : Dependencies.getClassDependencyFinder();
 294 
 295         }
 296 
 297         @Override
 298         public boolean accept(Archive archive, String cn, AccessFlags accessFlags) {
 299             int i = cn.lastIndexOf('.');
 300             String pn = i > 0 ? cn.substring(0, i) : "";
 301 
 302             // if -apionly is specified, analyze only exported and public types
 303             // All packages are exported in unnamed module.
 304             return apiOnly ? archive.getModule().isExported(pn) &&
 305                                  accessFlags.is(AccessFlags.ACC_PUBLIC)
 306                            : true;
 307         }
 308 
 309         @Override
 310         public Iterable<? extends Dependency> findDependencies(ClassFile classfile) {
 311             return finder.findDependencies(classfile);
 312         }
 313     }
 314 }