/* * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.tools.jdeps; import static com.sun.tools.jdeps.Module.*; import static com.sun.tools.jdeps.Analyzer.NOT_FOUND; import static java.util.stream.Collectors.*; import com.sun.tools.classfile.AccessFlags; import com.sun.tools.classfile.ClassFile; import com.sun.tools.classfile.ConstantPoolException; import com.sun.tools.classfile.Dependencies; import com.sun.tools.classfile.Dependencies.ClassFileError; import com.sun.tools.classfile.Dependency; import com.sun.tools.classfile.Dependency.Location; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Paths; import java.util.Collections; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.FutureTask; import java.util.stream.Stream; /** * Parses class files and finds dependences */ class DependencyFinder { private static Finder API_FINDER = new Finder(true); private static Finder CLASS_FINDER = new Finder(false); private final JdepsConfiguration configuration; private final JdepsFilter filter; private final Map> parsedArchives = new ConcurrentHashMap<>(); private final Map parsedClasses = new ConcurrentHashMap<>(); private final ExecutorService pool = Executors.newFixedThreadPool(2); private final Deque>> tasks = new ConcurrentLinkedDeque<>(); DependencyFinder(JdepsConfiguration configuration, JdepsFilter filter) { this.configuration = configuration; this.filter = filter; this.parsedArchives.put(API_FINDER, new ConcurrentLinkedDeque<>()); this.parsedArchives.put(CLASS_FINDER, new ConcurrentLinkedDeque<>()); } Map locationToArchive() { return parsedClasses; } /** * Returns the modules of all dependencies found */ Stream getDependences(Archive source) { return source.getDependencies() .map(this::locationToArchive) .filter(a -> a != source); } /** * Returns the location to archive map; or NOT_FOUND. * * Location represents a parsed class. */ Archive locationToArchive(Location location) { return parsedClasses.containsKey(location) ? parsedClasses.get(location) : configuration.findClass(location).orElse(NOT_FOUND); } /** * Returns a map from an archive to its required archives */ Map> dependences() { Map> map = new HashMap<>(); parsedArchives.values().stream() .flatMap(Deque::stream) .filter(a -> !a.isEmpty()) .forEach(source -> { Set deps = getDependences(source).collect(toSet()); if (!deps.isEmpty()) { map.put(source, deps); } }); return map; } boolean isParsed(Location location) { return parsedClasses.containsKey(location); } /** * Parses all class files from the given archive stream and returns * all target locations. */ public Set parse(Stream archiveStream) { archiveStream.forEach(archive -> parse(archive, CLASS_FINDER)); return waitForTasksCompleted(); } /** * Parses the exported API class files from the given archive stream and * returns all target locations. */ public Set parseExportedAPIs(Stream archiveStream) { archiveStream.forEach(archive -> parse(archive, API_FINDER)); return waitForTasksCompleted(); } /** * Parses the named class from the given archive and * returns all target locations the named class references. */ public Set parse(Archive archive, String name) { try { return parse(archive, CLASS_FINDER, name); } catch (IOException e) { throw new UncheckedIOException(e); } } /** * Parses the exported API of the named class from the given archive and * returns all target locations the named class references. */ public Set parseExportedAPIs(Archive archive, String name) { try { return parse(archive, API_FINDER, name); } catch (IOException e) { throw new UncheckedIOException(e); } } private Optional>> parse(Archive archive, Finder finder) { if (parsedArchives.get(finder).contains(archive)) return Optional.empty(); parsedArchives.get(finder).add(archive); trace("parsing %s %s%n", archive.getName(), archive.getPathName()); FutureTask> task = new FutureTask<>(() -> { Set targets = new HashSet<>(); for (ClassFile cf : archive.reader().getClassFiles()) { if (cf.access_flags.is(AccessFlags.ACC_MODULE)) continue; String classFileName; try { classFileName = cf.getName(); } catch (ConstantPoolException e) { throw new ClassFileError(e); } // filter source class/archive String cn = classFileName.replace('/', '.'); if (!finder.accept(archive, cn, cf.access_flags)) continue; // tests if this class matches the -include if (!filter.matches(cn)) continue; for (Dependency d : finder.findDependencies(cf)) { if (filter.accepts(d)) { archive.addClass(d.getOrigin(), d.getTarget()); targets.add(d.getTarget()); } else { // ensure that the parsed class is added the archive archive.addClass(d.getOrigin()); } parsedClasses.putIfAbsent(d.getOrigin(), archive); } } return targets; }); tasks.add(task); pool.submit(task); return Optional.of(task); } private Set parse(Archive archive, Finder finder, String name) throws IOException { ClassFile cf = archive.reader().getClassFile(name); if (cf == null) { throw new IllegalArgumentException(archive.getName() + " does not contain " + name); } if (cf.access_flags.is(AccessFlags.ACC_MODULE)) return Collections.emptySet(); Set targets = new HashSet<>(); String cn; try { cn = cf.getName().replace('/', '.'); } catch (ConstantPoolException e) { throw new Dependencies.ClassFileError(e); } if (!finder.accept(archive, cn, cf.access_flags)) return targets; // tests if this class matches the -include if (!filter.matches(cn)) return targets; // skip checking filter.matches for (Dependency d : finder.findDependencies(cf)) { if (filter.accepts(d)) { targets.add(d.getTarget()); archive.addClass(d.getOrigin(), d.getTarget()); } else { // ensure that the parsed class is added the archive archive.addClass(d.getOrigin()); } parsedClasses.putIfAbsent(d.getOrigin(), archive); } return targets; } /* * Waits until all submitted tasks are completed. */ private Set waitForTasksCompleted() { try { Set targets = new HashSet<>(); FutureTask> task; while ((task = tasks.poll()) != null) { // wait for completion targets.addAll(task.get()); } return targets; } catch (InterruptedException|ExecutionException e) { throw new Error(e); } } /* * Shutdown the executor service. */ void shutdown() { pool.shutdown(); } private interface SourceFilter { boolean accept(Archive archive, String cn, AccessFlags accessFlags); } private static class Finder implements Dependency.Finder, SourceFilter { private final Dependency.Finder finder; private final boolean apiOnly; Finder(boolean apiOnly) { this.apiOnly = apiOnly; this.finder = apiOnly ? Dependencies.getAPIFinder(AccessFlags.ACC_PROTECTED) : Dependencies.getClassDependencyFinder(); } @Override public boolean accept(Archive archive, String cn, AccessFlags accessFlags) { int i = cn.lastIndexOf('.'); String pn = i > 0 ? cn.substring(0, i) : ""; // if -apionly is specified, analyze only exported and public types // All packages are exported in unnamed module. return apiOnly ? archive.getModule().isExported(pn) && accessFlags.is(AccessFlags.ACC_PUBLIC) : true; } @Override public Iterable findDependencies(ClassFile classfile) { return finder.findDependencies(classfile); } } }