/* * Copyright 2015 Open Networking Laboratory * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.onlab.jdvue; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import static com.google.common.base.MoreObjects.toStringHelper; /** * Produces a package & source catalogue. * * @author Thomas Vachuska */ public class Catalog { private static final String PACKAGE = "package"; private static final String IMPORT = "import"; private static final String STATIC = "static"; private static final String SRC_ROOT = "src/main/java/"; private static final String WILDCARD = "\\.*$"; private final Map sources = new HashMap<>(); private final Map packages = new HashMap<>(); private final Set cycles = new HashSet<>(); private final Set cycleSegments = new HashSet<>(); private final Map> packageCycles = new HashMap<>(); private final Map> packageCycleSegments = new HashMap<>(); /** * Loads the catalog from the specified catalog file. * * @param catalogPath catalog file path * @throws IOException if unable to read the catalog file */ public void load(String catalogPath) throws IOException { InputStream is = new FileInputStream(catalogPath); BufferedReader br = new BufferedReader(new InputStreamReader(is)); String line; while ((line = br.readLine()) != null) { // Split the line into the two fields: path and pragmas String fields[] = line.trim().split(":"); if (fields.length <= 1) { continue; } String path = fields[0]; // Now split the pragmas on whitespace and trim punctuation String pragma[] = fields[1].trim().replaceAll("[;\n\r]", "").split("[\t ]"); // Locate (or create) Java source entity based on the path JavaSource source = getOrCreateSource(path); // Now process the package or import statements if (pragma[0].equals(PACKAGE)) { processPackageDeclaration(source, pragma[1]); } else if (pragma[0].equals(IMPORT)) { if (pragma[1].equals(STATIC)) { processImportStatement(source, pragma[2]); } else { processImportStatement(source, pragma[1]); } } } } /** * Analyzes the catalog by resolving imports and identifying circular * package dependencies. */ public void analyze() { resolveImports(); findCircularDependencies(); } /** * Identifies circular package dependencies through what amounts to be a * depth-first search rooted with each package. */ private void findCircularDependencies() { cycles.clear(); for (JavaPackage javaPackage : getPackages()) { findCircularDependencies(javaPackage); } cycleSegments.clear(); packageCycles.clear(); packageCycleSegments.clear(); for (DependencyCycle cycle : getCycles()) { recordCycleForPackages(cycle); cycleSegments.addAll(cycle.getCycleSegments()); } } /** * Records the specified cycle into a set for each involved package. * * @param cycle cycle to record for involved packages */ private void recordCycleForPackages(DependencyCycle cycle) { for (JavaPackage javaPackage : cycle.getCycle()) { Set cset = packageCycles.get(javaPackage); if (cset == null) { cset = new HashSet<>(); packageCycles.put(javaPackage, cset); } cset.add(cycle); Set sset = packageCycleSegments.get(javaPackage); if (sset == null) { sset = new HashSet<>(); packageCycleSegments.put(javaPackage, sset); } sset.addAll(cycle.getCycleSegments()); } } /** * Identifies circular dependencies in which this package participates * using depth-first search. * * @param javaPackage Java package to inspect for dependency cycles */ private void findCircularDependencies(JavaPackage javaPackage) { // Setup a depth trace anchored at the given java package. List trace = newTrace(new ArrayList(), javaPackage); Set searched = new HashSet<>(); searchDependencies(javaPackage, trace, searched); } /** * Generates a new trace using the previous one and a new element * * @param trace old search trace * @param javaPackage package to add to the trace * @return new search trace */ private List newTrace(List trace, JavaPackage javaPackage) { List newTrace = new ArrayList<>(trace); newTrace.add(javaPackage); return newTrace; } /** * Recursive depth-first search through dependency tree * * @param javaPackage java package being searched currently * @param trace search trace * @param searched set of java packages already searched */ private void searchDependencies(JavaPackage javaPackage, List trace, Set searched) { if (!searched.contains(javaPackage)) { searched.add(javaPackage); for (JavaPackage dependency : javaPackage.getDependencies()) { if (trace.contains(dependency)) { cycles.add(new DependencyCycle(trace, dependency)); } else { searchDependencies(dependency, newTrace(trace, dependency), searched); } } } } /** * Resolves import names of Java sources into imports of entities known * to this catalog. All other import names will be ignored. */ private void resolveImports() { for (JavaPackage javaPackage : getPackages()) { Set dependencies = new HashSet<>(); for (JavaSource source : javaPackage.getSources()) { Set imports = resolveImports(source); source.setImports(imports); dependencies.addAll(importedPackages(imports)); } javaPackage.setDependencies(dependencies); } } /** * Produces a set of imported Java packages from the specified set of * Java source entities. * * @param imports list of imported Java source entities * @return list of imported Java packages */ private Set importedPackages(Set imports) { Set packages = new HashSet<>(); for (JavaEntity entity : imports) { packages.add(entity instanceof JavaPackage ? (JavaPackage) entity : ((JavaSource) entity).getPackage()); } return packages; } /** * Resolves import names of the specified Java source into imports of * entities known to this catalog. All other import names will be ignored. * * @param source Java source * @return list of resolved imports */ private Set resolveImports(JavaSource source) { Set imports = new HashSet<>(); for (String importName : source.getImportNames()) { JavaEntity entity = importName.matches(WILDCARD) ? getPackage(importName.replaceAll(WILDCARD, "")) : getSource(importName); if (entity != null) { imports.add(entity); } } return imports; } /** * Returns either an existing or a newly created Java package. * * @param packageName Java package name * @return Java package */ private JavaPackage getOrCreatePackage(String packageName) { JavaPackage javaPackage = packages.get(packageName); if (javaPackage == null) { javaPackage = new JavaPackage(packageName); packages.put(packageName, javaPackage); } return javaPackage; } /** * Returns either an existing or a newly created Java source. * * @param path Java source path * @return Java source */ private JavaSource getOrCreateSource(String path) { String name = nameFromPath(path); JavaSource source = sources.get(name); if (source == null) { source = new JavaSource(name, path); sources.put(name, source); } return source; } /** * Extracts a fully qualified source class name from the given path. *

* For now, this implementation assumes standard Maven source structure * and thus will look for start of package name under 'src/main/java/'. * If it will not find such a prefix, it will simply return the path as * the name. * * @param path source path * @return source name */ private String nameFromPath(String path) { int i = path.indexOf(SRC_ROOT); String name = i < 0 ? path : path.substring(i + SRC_ROOT.length()); return name.replaceAll("\\.java$", "").replace("/", "."); } /** * Processes the package declaration pragma for the given source. * * @param source Java source * @param packageName Java package name */ private void processPackageDeclaration(JavaSource source, String packageName) { JavaPackage javaPackage = getOrCreatePackage(packageName); source.setPackage(javaPackage); javaPackage.addSource(source); } /** * Processes the import pragma for the given source. * * @param source Java source * @param name name of the Java entity being imported (class or package) */ private void processImportStatement(JavaSource source, String name) { source.addImportName(name); } /** * Returns the collection of java sources. * * @return collection of java sources */ public Collection getSources() { return Collections.unmodifiableCollection(sources.values()); } /** * Returns the Java source with the specified name. * * @param name Java source name * @return Java source */ public JavaSource getSource(String name) { return sources.get(name); } /** * Returns the collection of all Java packages. * * @return collection of java packages */ public Collection getPackages() { return Collections.unmodifiableCollection(packages.values()); } /** * Returns the set of all Java package dependency cycles. * * @return set of dependency cycles */ public Set getCycles() { return Collections.unmodifiableSet(cycles); } /** * Returns the set of all Java package dependency cycle segments. * * @return set of dependency cycle segments */ public Set getCycleSegments() { return Collections.unmodifiableSet(cycleSegments); } /** * Returns the set of dependency cycles which involve the specified package. * * @param javaPackage java package * @return set of dependency cycles */ public Set getPackageCycles(JavaPackage javaPackage) { Set set = packageCycles.get(javaPackage); return Collections.unmodifiableSet(set == null ? new HashSet() : set); } /** * Returns the set of dependency cycle segments which involve the specified package. * * @param javaPackage java package * @return set of dependency cycle segments */ public Set getPackageCycleSegments(JavaPackage javaPackage) { Set set = packageCycleSegments.get(javaPackage); return Collections.unmodifiableSet(set == null ? new HashSet() : set); } /** * Returns the Java package with the specified name. * * @param name Java package name * @return Java package */ public JavaPackage getPackage(String name) { return packages.get(name); } @Override public String toString() { return toStringHelper(this) .add("packages", packages.size()) .add("sources", sources.size()) .add("cycles", cycles.size()) .add("cycleSegments", cycleSegments.size()).toString(); } }