| /* |
| * 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<String, JavaSource> sources = new HashMap<>(); |
| private final Map<String, JavaPackage> packages = new HashMap<>(); |
| private final Set<DependencyCycle> cycles = new HashSet<>(); |
| private final Set<Dependency> cycleSegments = new HashSet<>(); |
| private final Map<JavaPackage, Set<DependencyCycle>> packageCycles = new HashMap<>(); |
| private final Map<JavaPackage, Set<Dependency>> 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<DependencyCycle> cset = packageCycles.get(javaPackage); |
| if (cset == null) { |
| cset = new HashSet<>(); |
| packageCycles.put(javaPackage, cset); |
| } |
| cset.add(cycle); |
| |
| Set<Dependency> 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<JavaPackage> trace = newTrace(new ArrayList<JavaPackage>(), javaPackage); |
| |
| Set<JavaPackage> 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<JavaPackage> newTrace(List<JavaPackage> trace, |
| JavaPackage javaPackage) { |
| List<JavaPackage> 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<JavaPackage> trace, |
| Set<JavaPackage> 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<JavaPackage> dependencies = new HashSet<>(); |
| for (JavaSource source : javaPackage.getSources()) { |
| Set<JavaEntity> 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<JavaPackage> importedPackages(Set<JavaEntity> imports) { |
| Set<JavaPackage> 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<JavaEntity> resolveImports(JavaSource source) { |
| Set<JavaEntity> 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. |
| * <p/> |
| * 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<JavaSource> 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<JavaPackage> getPackages() { |
| return Collections.unmodifiableCollection(packages.values()); |
| } |
| |
| /** |
| * Returns the set of all Java package dependency cycles. |
| * |
| * @return set of dependency cycles |
| */ |
| public Set<DependencyCycle> getCycles() { |
| return Collections.unmodifiableSet(cycles); |
| } |
| |
| /** |
| * Returns the set of all Java package dependency cycle segments. |
| * |
| * @return set of dependency cycle segments |
| */ |
| public Set<Dependency> 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<DependencyCycle> getPackageCycles(JavaPackage javaPackage) { |
| Set<DependencyCycle> set = packageCycles.get(javaPackage); |
| return Collections.unmodifiableSet(set == null ? new HashSet<DependencyCycle>() : 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<Dependency> getPackageCycleSegments(JavaPackage javaPackage) { |
| Set<Dependency> set = packageCycleSegments.get(javaPackage); |
| return Collections.unmodifiableSet(set == null ? new HashSet<Dependency>() : 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(); |
| } |
| |
| } |