Thomas Vachuska

Moved jdvue utility from ONOS-tools repo into onos repo.

Change-Id: I0bc1cef80541075c800c5309cb642a244a79fa0b
#!/bin/bash
#-------------------------------------------------------------------------------
# Java Package Dependency viewer
#
# written by Thomas Vachuska
# -- Doobs --
#-------------------------------------------------------------------------------
JDVUE_ROOT=${JDVUE_ROOT:-$(dirname $0)/..}
cd $JDVUE_ROOT
VER=1.2.0-SNAPSHOT
JAR=$PWD/target/jdvue-${VER}.jar # start with the dev jar first
cd - >/dev/null
# If the dev jar is not available, use one from .m2/repository
[ -f ${JAR} ] || JAR=~/.m2/repository/org/onlab/tools/jdvue/${VER}/jdvue-${VER}.jar
# Assume default project to be the base-name of the argument or of current dir
name=$(basename ${1:-$PWD})
# If the -n option is specified use the next argument as the catalog name
[ "$1" = "-n" -a $# -ge 2 ] && name=$2 && shift 2
# Use the rest of the arguments as paths to scan for sources to build catalog
find "${@:-.}" -type f -name \*.java \
| grep -v -E '/lost+found/|/target/|archetype-resources' \
| xargs grep -E "^[ \t]*import .*;.*|^[ \t]*package .*;.*" \
| tr -d '\r' > $name.db
# Now run the Java Dependency Viewer jar on the catalog
java -jar ${JAR} $name && rm $name.db && open $name.html
#!/bin/bash
#-------------------------------------------------------------------------------
# Java Package Dependency scanner
#
# written by Thomas Vachuska
# -- Doobs --
#-------------------------------------------------------------------------------
find "${@:-.}" -type f -name \*.java \
| grep -v -E '/lost+found/|/target/' \
| xargs grep -E "^[ \t]*import .*;.*|^[ \t]*package .*;.*" \
| tr -d '\r' > jpd.db
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<prerequisites>
<maven>3.0.4</maven>
</prerequisites>
<parent>
<groupId>org.onosproject</groupId>
<artifactId>onos-base</artifactId>
<version>1</version>
<relativePath>../../build/pom.xml</relativePath>
</parent>
<artifactId>jdvue</artifactId>
<version>1.2.0-SNAPSHOT</version>
<packaging>jar</packaging>
<description>Java Package Dependency &amp; Analyzer</description>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava-testlib</artifactId>
<version>18.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.3</version>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.onlab.jdvue.DependencyViewer</mainClass>
</transformer>
</transformers>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
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.Objects.toStringHelper;
/**
* Produces a package &amp; 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
*/
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();
}
}
package org.onlab.jdvue;
import java.util.Objects;
import static com.google.common.base.Objects.toStringHelper;
/**
* Abstraction of a dependency segment.
*
* @author Thomas Vachuska
*/
public class Dependency {
private final JavaPackage source;
private final JavaPackage target;
/**
* Creates a dependency from the specified source on the given target.
*
* @param source source of the dependency
* @param target target of the dependency
*/
public Dependency(JavaPackage source, JavaPackage target) {
this.source = source;
this.target = target;
}
/**
* Returns the dependency source.
*
* @return source Java package
*/
public JavaPackage getSource() {
return source;
}
/**
* Returns the dependency target.
*
* @return target Java package
*/
public JavaPackage getTarget() {
return target;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Dependency) {
Dependency that = (Dependency) obj;
return Objects.equals(source, that.source) &&
Objects.equals(target, that.target);
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(source, target);
}
@Override
public String toString() {
return toStringHelper(this)
.add("source", source).add("target", target).toString();
}
}
package org.onlab.jdvue;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import static com.google.common.base.Objects.toStringHelper;
/**
* Simple representation of a Java package dependency cycle.
*/
public class DependencyCycle {
private final List<JavaPackage> cycle;
/**
* Creates a normalized dependency cycle represented by the specified list
* of Java packages, which are expected to be given in order of dependency.
* List is assumed to be non-empty.
*
* @param cycle list of Java packages in the dependency cycle
* @param cause Java package that caused the cycle
*/
DependencyCycle(List<JavaPackage> cycle, JavaPackage cause) {
this.cycle = normalize(cycle, cause);
}
/**
* Produces a normalized dependency cycle list. Normalization is performed
* by rotating the list so that the package with the least lexicographic
* name is at the start of the list.
*
* @param cycle list of Java packages in the dependency cycle
* @param cause Java package that caused the cycle
* @return normalized cycle
*/
private List<JavaPackage> normalize(List<JavaPackage> cycle, JavaPackage cause) {
int start = cycle.indexOf(cause);
List<JavaPackage> clone = new ArrayList<>(cycle.subList(start, cycle.size()));
int leastIndex = findIndexOfLeastName(clone);
Collections.rotate(clone, -leastIndex);
return Collections.unmodifiableList(clone);
}
/**
* Returns the index of the Java package with the least name.
*
* @param cycle list of Java packages in the dependency cycle
* @return index of the least Java package name
*/
private int findIndexOfLeastName(List<JavaPackage> cycle) {
int leastIndex = 0;
String leastName = cycle.get(leastIndex).name();
for (int i = 1, n = cycle.size(); i < n; i++) {
JavaPackage javaPackage = cycle.get(i);
if (leastName.compareTo(javaPackage.name()) > 0) {
leastIndex = i;
leastName = javaPackage.name();
}
}
return leastIndex;
}
/**
* Returns the normalized Java package dependency cycle
*
* @return list of packages in the dependency cycle
*/
public List<JavaPackage> getCycle() {
return cycle;
}
/**
* Returns the dependency cycle in form of individual dependencies.
*
* @return list of dependencies forming the cycle
*/
public List<Dependency> getCycleSegments() {
List<Dependency> dependencies = new ArrayList<>();
for (int i = 0, n = cycle.size(); i < n; i++) {
dependencies.add(new Dependency(cycle.get(i), cycle.get(i < n - 1 ? i + 1 : 0)));
}
return dependencies;
}
@Override
public boolean equals(Object o) {
if (o instanceof DependencyCycle) {
DependencyCycle that = (DependencyCycle) o;
return Objects.equals(cycle, that.cycle);
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(cycle);
}
@Override
public String toString() {
return toStringHelper(this).add("cycle", cycle).toString();
}
public String toShortString() {
StringBuilder sb = new StringBuilder("[");
for (JavaPackage javaPackage : cycle) {
sb.append(javaPackage.name()).append(", ");
}
if (sb.length() > 1) {
sb.delete(sb.length() - 2, sb.length());
}
sb.append("]");
return sb.toString();
}
}
package org.onlab.jdvue;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.BufferedReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Set;
/**
* Generator of a self-contained HTML file which serves as a GUI for
* visualizing Java package dependencies carried in the supplied catalog.
*
* The HTML file is an adaptation of D3.js Hierarchical Edge Bundling as
* shown at http://bl.ocks.org/mbostock/7607999.
*
* @author Thomas Vachuska
*/
public class DependencyViewer {
private static final String JPD_EXT = ".db";
private static final String HTML_EXT = ".html";
private static final String INDEX = "index.html";
private static final String D3JS = "d3.v3.min.js";
private static final String TITLE_PLACEHOLDER = "TITLE_PLACEHOLDER";
private static final String D3JS_PLACEHOLDER = "D3JS_PLACEHOLDER";
private static final String DATA_PLACEHOLDER = "DATA_PLACEHOLDER";
private final Catalog catalog;
/**
* Creates a Java package dependency viewer.
*
* @param catalog dependency catalog
*/
public DependencyViewer(Catalog catalog) {
this.catalog = catalog;
}
/**
* Main program entry point.
*
* @param args command line arguments
*/
public static void main(String[] args) {
Catalog cat = new Catalog();
DependencyViewer viewer = new DependencyViewer(cat);
try {
String path = args[0];
cat.load(path + JPD_EXT);
cat.analyze();
System.err.println(cat);
viewer.dumpLongestCycle(cat);
viewer.writeHTMLFile(path);
} catch (IOException e) {
System.err.println("Unable to process catalog: " + e.getMessage());
}
}
/**
* Prints out the longest cycle; just for kicks.
* @param cat catalog
*/
private void dumpLongestCycle(Catalog cat) {
DependencyCycle longest = null;
for (DependencyCycle cycle : cat.getCycles()) {
if (longest == null || longest.getCycleSegments().size() < cycle.getCycleSegments().size()) {
longest = cycle;
}
}
if (longest != null) {
for (Dependency dependency : longest.getCycleSegments()) {
System.out.println(dependency);
}
}
}
/**
* Writes the HTML catalog file for the given viewer.
*
* @param path base file path
* @throws IOException if issues encountered writing the HTML file
*/
public void writeHTMLFile(String path) throws IOException {
String index = slurp(getClass().getResourceAsStream(INDEX));
String d3js = slurp(getClass().getResourceAsStream(D3JS));
FileWriter fw = new FileWriter(path + HTML_EXT);
ObjectWriter writer = new ObjectMapper().writer(); // .writerWithDefaultPrettyPrinter();
fw.write(index.replace(TITLE_PLACEHOLDER, path)
.replace(D3JS_PLACEHOLDER, d3js)
.replace(DATA_PLACEHOLDER, writer.writeValueAsString(toJson())));
fw.close();
}
/**
* Slurps the specified input stream into a string.
*
* @param stream input stream to be read
* @return string containing the contents of the input stream
* @throws IOException if issues encountered reading from the stream
*/
static String slurp(InputStream stream) throws IOException {
StringBuilder sb = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(stream));
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append(System.lineSeparator());
}
br.close();
return sb.toString();
}
// Produces a JSON structure designed to drive the hierarchical visual
// representation of Java package dependencies and any dependency cycles
private JsonNode toJson() {
ObjectMapper mapper = new ObjectMapper();
ObjectNode root = mapper.createObjectNode();
root.put("packages", jsonPackages(mapper));
root.put("cycleSegments", jsonCycleSegments(mapper, catalog.getCycleSegments()));
root.put("summary", jsonSummary(mapper));
return root;
}
// Produces a JSON summary of dependencies
private JsonNode jsonSummary(ObjectMapper mapper) {
ObjectNode summary = mapper.createObjectNode();
summary.put("packages", catalog.getPackages().size());
summary.put("sources", catalog.getSources().size());
summary.put("cycles", catalog.getCycles().size());
summary.put("cycleSegments", catalog.getCycleSegments().size());
return summary;
}
// Produces a JSON structure with package dependency data
private JsonNode jsonPackages(ObjectMapper mapper) {
ArrayNode packages = mapper.createArrayNode();
for (JavaPackage javaPackage : catalog.getPackages()) {
packages.add(json(mapper, javaPackage));
}
return packages;
}
// Produces a JSON structure with all cyclic segments
private JsonNode jsonCycleSegments(ObjectMapper mapper,
Set<Dependency> segments) {
ObjectNode cyclicSegments = mapper.createObjectNode();
for (Dependency dependency : segments) {
String s = dependency.getSource().name();
String t = dependency.getTarget().name();
cyclicSegments.put(t + "-" + s,
mapper.createObjectNode().put("s", s).put("t", t));
}
return cyclicSegments;
}
// Produces a JSON object structure describing the specified Java package.
private JsonNode json(ObjectMapper mapper, JavaPackage javaPackage) {
ObjectNode node = mapper.createObjectNode();
ArrayNode imports = mapper.createArrayNode();
for (JavaPackage dependency : javaPackage.getDependencies()) {
imports.add(dependency.name());
}
Set<DependencyCycle> packageCycles = catalog.getPackageCycles(javaPackage);
Set<Dependency> packageCycleSegments = catalog.getPackageCycleSegments(javaPackage);
node.put("name", javaPackage.name());
node.put("size", javaPackage.getSources().size());
node.put("imports", imports);
node.put("cycleSegments", jsonCycleSegments(mapper, packageCycleSegments));
node.put("cycleCount", packageCycles.size());
node.put("cycleSegmentCount", packageCycleSegments.size());
return node;
}
}
package org.onlab.jdvue;
import java.util.Objects;
/**
* Abstraction of a Java source entity.
*/
public abstract class JavaEntity {
private final String name;
/**
* Creates a new Java source entity with the given name.
*
* @param name source entity name
*/
JavaEntity(String name) {
this.name = name;
}
/**
* Returns the Java source entity name.
*
* @return source entity name
*/
public String name() {
return name;
}
@Override
public boolean equals(Object o) {
if (o instanceof JavaEntity) {
JavaEntity that = (JavaEntity) o;
return getClass().equals(that.getClass()) &&
Objects.equals(name, that.name);
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
package org.onlab.jdvue;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import static com.google.common.base.Objects.toStringHelper;
/**
* Simple abstraction of a Java package for the purpose of tracking
* dependencies and requirements.
*
* @author Thomas Vachuska
*/
public class JavaPackage extends JavaEntity {
private final Set<JavaSource> sources = new HashSet<>();
private Set<JavaPackage> dependencies;
/**
* Creates a new Java package.
*
* @param name java package file name
*/
JavaPackage(String name) {
super(name);
}
/**
* Returns the set of sources contained in this Java package.
*
* @return set of Java sources
*/
public Set<JavaSource> getSources() {
return Collections.unmodifiableSet(sources);
}
/**
* Adds the specified Java source to the package. Only possible if the
* Java package of the source is the same as this Java package.
*
* @param source Java source to be added
*/
void addSource(JavaSource source) {
if (source.getPackage().equals(this)) {
sources.add(source);
}
}
/**
* Returns the set of packages directly required by this package.
*
* @return set of Java package dependencies
*/
Set<JavaPackage> getDependencies() {
return dependencies;
}
/**
* Sets the set of resolved Java packages on which this package dependens.
*
* @param dependencies set of resolved Java packages
*/
void setDependencies(Set<JavaPackage> dependencies) {
if (this.dependencies == null) {
this.dependencies = Collections.unmodifiableSet(new HashSet<>(dependencies));
}
}
@Override
public String toString() {
return toStringHelper(this)
.add("name", name())
.add("sources", sources.size())
.add("dependencies", (dependencies != null ? dependencies.size() : 0))
.toString();
}
}
package org.onlab.jdvue;
import java.util.*;
import static com.google.common.base.Objects.toStringHelper;
/**
* Simple abstraction of a Java source file for the purpose of tracking
* dependencies and requirements.
*
* @author Thomas Vachuska
*/
public class JavaSource extends JavaEntity {
private String path;
private JavaPackage javaPackage;
private final Set<String> importNames = new HashSet<>();
private Set<JavaEntity> imports;
/**
* Creates a new Java source entity.
*
* @param name java source file name
*/
JavaSource(String name, String path) {
super(name);
this.path = path;
}
/**
* Returns the Java package for this Java source.
*
* @return Java package
*/
public JavaPackage getPackage() {
return javaPackage;
}
/**
* Sets the Java package for this Java source.
*
* @param javaPackage Java package
*/
void setPackage(JavaPackage javaPackage) {
if (this.javaPackage == null) {
this.javaPackage = javaPackage;
}
}
/**
* Returns the set of resolved imports for this Java source
* @return set of imports
*/
public Set<JavaEntity> getImports() {
return imports;
}
/**
* Sets the set of resolved imported Java entities for this source.
*
* @param imports set of resolved Java entities imported by this source
*/
void setImports(Set<JavaEntity> imports) {
if (this.imports == null) {
this.imports = Collections.unmodifiableSet(new HashSet<>(imports));
}
}
/**
* Adds a name of an imported, but unresolved, Java entity name.
*
* @param name name of an imported Java entity
*/
void addImportName(String name) {
importNames.add(name);
}
/**
* Returns the set of imported, but unresolved, Java entity names.
* @return set of imported Java entity names
*/
Set<String> getImportNames() {
return importNames;
}
@Override
public String toString() {
return toStringHelper(this)
.add("name", name())
.add("javaPackage", (javaPackage != null ? javaPackage.name() : ""))
.add("importNames", importNames.size())
.add("imports", (imports != null ? imports.size() : 0))
.toString();
}
}
/*
* 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.
*/
/**
* Utility to analyze Java package dependencies.
*/
package org.onlab.jdvue;
\ No newline at end of file
This diff could not be displayed because it is too large.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>TITLE_PLACEHOLDER</title>
<style>
.node {
font: 300 13px "Helvetica Neue", Helvetica, Arial, sans-serif;
fill: #bbb;
}
.link {
stroke: steelblue;
stroke-opacity: .4;
fill: none;
pointer-events: none;
}
.node--focus {
font-weight: 700;
fill: #000;
}
.node:hover {
fill: steelblue;
}
.node:hover,
.node--source,
.node--target {
font-weight: 700;
}
.node--source {
fill: #2ca02c;
}
.node--target {
fill: #d59800;
}
.link--source,
.link--target {
stroke-opacity: 1;
stroke-width: 3px;
}
.link--source {
stroke: #d59800;
}
.link--target {
stroke: #2ca02c;
}
.link--cycle {
stroke: #ff0000;
}
.summary {
font: 300 13px "Helvetica Neue", Helvetica, Arial, sans-serif;
position: fixed;
top: 32px;
right: 32px;
width: 192px;
background-color: #ffffff;
box-shadow: 2px 2px 4px 2px #777777;
padding: 5px;
}
.details {
display: none;
font: 300 13px "Helvetica Neue", Helvetica, Arial, sans-serif;
position: fixed;
top: 220px;
right: 32px;
width: 192px;
background-color: #ffffff;
box-shadow: 2px 2px 4px 2px #777777;
padding: 5px;
}
.shown {
display:block;
}
.stat {
text-align: right;
width: 64px;
}
.title {
font-size: 16px;
font-weight: bold;
}
#package {
font-size: 14px;
font-weight: bold;
}
</style>
</head>
<body>
<div class="summary">
<div class="title">Project TITLE_PLACEHOLDER</div>
<table>
<tr>
<td>Sources:</td>
<td id="sourceCount" class="stat"></td>
</tr>
<tr>
<td>Packages:</td>
<td id="packageCount" class="stat"></td>
</tr>
<tr>
<td>Cyclic Segments:</td>
<td id="segmentCount" class="stat"></td>
</tr>
<tr>
<td>Cycles:</td>
<td id="cycleCount" class="stat"></td>
</tr>
</table>
<div><hr size="1"></div>
<div><input type="checkbox"> Highlight cycles</input></div>
<div><input style="width: 95%" type="range" min="0" max="100" value="75"></div>
</div>
<div class="details">
<div id="package">Package Details</div>
<table>
<tr>
<td>Sources:</td>
<td id="psourceCount" class="stat"></td>
</tr>
<tr>
<td>Dependents:</td>
<td id="pdependentCount" class="stat"></td>
</tr>
<tr>
<td>Cyclic Segments:</td>
<td id="psegmentCount" class="stat"></td>
</tr>
<tr>
<td>Cycles:</td>
<td id="pcycleCount" class="stat"></td>
</tr>
</table>
</div>
<script>
D3JS_PLACEHOLDER
var catalog =
DATA_PLACEHOLDER
;
var diameter = 1000,
radius = diameter / 2,
innerRadius = radius - 300;
var cluster = d3.layout.cluster()
.size([360, innerRadius])
.sort(null)
.value(function(d) { return d.size; });
var bundle = d3.layout.bundle();
var line = d3.svg.line.radial()
.interpolate("bundle")
.tension(.75)
.radius(function(d) { return d.y; })
.angle(function(d) { return d.x / 180 * Math.PI; });
var svg = d3.select("body").append("svg")
.attr("width", diameter)
.attr("height", diameter)
.append("g")
.attr("transform", "translate(" + radius + "," + radius + ")");
var link = svg.append("g").selectAll(".link"),
node = svg.append("g").selectAll(".node"),
cycles = {}, highlightCycles, selectedNode;
function isCyclicLink(l) {
return highlightCycles &&
(cycles[l.source.key + "-" + l.target.key] || cycles[l.target.key + "-" + l.source.key]);
}
function isCyclicPackageLink(l, p) {
var key = l.source.key + "-" + l.target.key,
rKey = l.target.key + "-" + l.source.key;
return isCyclicLink(l) && (p.cycleSegments[key] || p.cycleSegments[rKey]);
}
function refreshPaths() {
svg.selectAll("path.link").classed("link--cycle", isCyclicLink);
}
function processCatalog() {
var nodes = cluster.nodes(packageHierarchy(catalog.packages)),
links = packageImports(nodes),
splines = bundle(links);
cycles = catalog.cycleSegments;
d3.select("input[type=checkbox]").on("change", function() {
highlightCycles = this.checked;
refreshPaths();
});
link = link
.data(splines)
.enter().append("path")
.each(function(d) { d.source = d[0], d.target = d[d.length - 1]; })
.attr("class", "link")
.classed("link--cycle", isCyclicLink)
.attr("d", function(d, i) { return line(splines[i]); });
node = node
.data(nodes.filter(function(n) { return !n.children; }))
.enter().append("text")
.attr("class", "node")
.attr("dy", ".31em")
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + (d.y + 8) + ",0)" + (d.x < 180 ? "" : "rotate(180)"); })
.style("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
.text(function(d) { return d.key; })
.on("focus", processSelect)
.on("blur", processSelect);
d3.select("input[type=range]").on("change", function() {
line.tension(this.value / 100);
svg.selectAll("path.link")
.data(splines)
.attr("d", function(d, i) { return line(splines[i]); });
});
d3.select("#packageCount").text(catalog.summary.packages);
d3.select("#sourceCount").text(catalog.summary.sources);
d3.select("#segmentCount").text(catalog.summary.cycleSegments);
d3.select("#cycleCount").text(catalog.summary.cycles);
}
function processSelect(d) {
if (selectedNode === d) {
deselected(d);
selectedNode = null;
} else if (selectedNode) {
deselected(selectedNode);
selectedNode = null;
selected(d);
} else {
selected(d);
selectedNode = d;
}
}
function selected(d) {
node
.each(function(n) { n.target = n.source = false; })
.classed("node--focus", function(n) { return n === d; });
link
.classed("link--cycle", function(l) { return isCyclicPackageLink(l, d); })
.classed("link--target", function(l) { if (l.target === d) return l.source.source = true; })
.classed("link--source", function(l) { if (l.source === d) return l.target.target = true; })
.filter(function(l) { return l.target === d || l.source === d; })
.each(function() { this.parentNode.appendChild(this); });
node
.classed("node--target", function(n) { return n.target; })
.classed("node--source", function(n) { return n.source; });
d3.select("#psourceCount").text(d.size);
d3.select("#pdependentCount").text(d.imports.length);
d3.select("#psegmentCount").text(d.cycleSegmentCount);
d3.select("#pcycleCount").text(d.cycleCount);
d3.select(".details").classed("shown", function() { return true; });
}
function deselected(d) {
link
.classed("link--cycle", isCyclicLink)
.classed("link--target", false)
.classed("link--source", false);
node
.classed("node--target", false)
.classed("node--source", false)
.classed("node--focus", false);
d3.select(".details").classed("shown", function() { return false; });
}
d3.select(self.frameElement).style("height", diameter + "px");
// Lazily construct the package hierarchy.
function packageHierarchy(packages) {
var map = {}, cnt = 0;
// Builds the structure top-down to the specified leaf or until
// another leaf in which case hook this leaf to the same parent
function buildHierarchy(leaf, i) {
var leafName = leaf.name,
node, name, parent = map[""], start = 0;
while (start < leafName.length) {
name = parentName(leafName, start);
node = map[name];
if (!node) {
node = map[name] = parentNode(name, parent);
parent.children.push(node);
} else if (node.imports) {
leaf.parent = parent;
parent.children.push(leaf);
break;
}
parent = node;
start = name.length + 1;
}
}
function parentNode(name, parent) {
return {name: name, parent: parent, key: name, children: []};
}
function parentName(leafName, start) {
var i = leafName.indexOf(".", start);
return i > 0 ? leafName.substring(0, i) : leafName;
}
// First populate all packages as leafs
packages.forEach(function(d) {
map[d.name] = d;
d.key = d.name;
});
// Next synthesize the intermediate structure, by-passing any leafs
map[""] = parentNode("", null);
var i = 0;
packages.forEach(function(d) {
buildHierarchy(d, i++);
});
return map[""];
}
// Return a list of imports for the given array of nodes.
function packageImports(nodes) {
var map = {},
imports = [];
// Compute a map from name to node.
nodes.forEach(function(d) {
map[d.name] = d;
});
// For each import, construct a link from the source to target node.
nodes.forEach(function(d) {
if (d.imports) d.imports.forEach(function(i) {
imports.push({source: map[d.name], target: map[i]});
});
});
return imports;
}
processCatalog();
</script>
</body>
</html>
package org.onlab.jdvue;
import org.junit.Test;
import org.onlab.jdvue.Catalog;
import org.onlab.jdvue.JavaPackage;
import org.onlab.jdvue.JavaSource;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
/**
* Unit test for the source catalog.
*
* @author Thomas Vachuska
*/
public class CatalogTest {
@Test
public void basics() throws IOException {
Catalog cat = new Catalog();
cat.load("src/test/resources/catalog.db");
cat.analyze();
assertEquals("incorrect package count", 12, cat.getPackages().size());
assertEquals("incorrect source count", 14, cat.getSources().size());
JavaPackage pkg = cat.getPackage("k");
assertNotNull("package should be found", pkg);
JavaSource src = cat.getSource("k.K");
assertNotNull("source should be found", src);
assertEquals("incorrect package source count", 1, pkg.getSources().size());
assertEquals("incorrect package dependency count", 1, pkg.getDependencies().size());
assertEquals("incorrect package cycle count", 3, cat.getPackageCycles(pkg).size());
assertEquals("incorrect segment count", 11, cat.getCycleSegments().size());
assertEquals("incorrect cycle count", 5, cat.getCycles().size());
}
}
package org.onlab.jdvue;
import org.junit.Test;
import org.onlab.jdvue.DependencyCycle;
import org.onlab.jdvue.JavaPackage;
import java.util.Arrays;
import static org.junit.Assert.*;
/**
* Unit test for the dependency cycle entity.
*
* @author Thomas Vachuska
*/
public class DependencyCycleTest {
@Test
public void normalize() {
JavaPackage x = new JavaPackage("x");
JavaPackage y = new JavaPackage("y");
JavaPackage z = new JavaPackage("z");
DependencyCycle a = new DependencyCycle(Arrays.asList(new JavaPackage[] {x, y, z}), x);
DependencyCycle b = new DependencyCycle(Arrays.asList(new JavaPackage[] {y, z, x}), y);
DependencyCycle c = new DependencyCycle(Arrays.asList(new JavaPackage[] {z, x, y}), z);
assertEquals("incorrect normalization", a, b);
assertEquals("incorrect normalization", a, c);
}
@Test
public void testToString() {
JavaPackage x = new JavaPackage("x");
JavaPackage y = new JavaPackage("y");
JavaPackage z = new JavaPackage("z");
DependencyCycle a = new DependencyCycle(Arrays.asList(new JavaPackage[] {x, y, z}), x);
assertEquals("incorrect toString", "[x, y, z]", a.toShortString());
assertEquals("incorrect toString",
"DependencyCycle{cycle=[" +
"JavaPackage{name=x, sources=0, dependencies=0}, " +
"JavaPackage{name=y, sources=0, dependencies=0}, " +
"JavaPackage{name=z, sources=0, dependencies=0}]}",
a.toString());
}
}
package org.onlab.jdvue;
import com.google.common.testing.EqualsTester;
import org.junit.Test;
import org.onlab.jdvue.Dependency;
import org.onlab.jdvue.JavaPackage;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
/**
* Unit test for the dependency entity.
*
* @author Thomas Vachuska
*/
public class DependencyTest {
@Test
public void basics() {
JavaPackage x = new JavaPackage("x");
JavaPackage y = new JavaPackage("y");
new EqualsTester()
.addEqualityGroup(new Dependency(x, y), new Dependency(x, y))
.addEqualityGroup(new Dependency(y, x), new Dependency(y, x))
.testEquals();
}
}
package org.onlab.jdvue;
import org.junit.Test;
import org.onlab.jdvue.DependencyViewer;
import java.io.FileInputStream;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.onlab.jdvue.DependencyViewer.slurp;
/**
* Unit test for the dependency viewer.
*
* @author Thomas Vachuska
*/
public class DependencyViewerTest {
@Test
public void basics() throws IOException {
DependencyViewer.main(new String[]{"src/test/resources/catalog"});
String expected = slurp(new FileInputStream("src/test/resources/expected.html"));
String actual = slurp(new FileInputStream("src/test/resources/catalog.html"));
// FIXME: add more manageable assertions here
// assertEquals("incorrect html", expected, actual);
}
}
src/main/java/a/A.java:package a;
src/main/java/a/A.java:import b.B;
src/main/java/a/A2.java:package a;
src/main/java/a/A2.java:import c.C;
src/main/java/b/B.java:package b;
src/main/java/b/B.java:import c.C;
src/main/java/c/C.java:package c;
src/main/java/x/X.java:package x;
src/main/java/x/X.java:import y.Y;
src/main/java/y/Y.java:package y;
src/main/java/y/Y.java:import z.Z;
src/main/java/z/Z.java:package z;
src/main/java/z/Z.java:import x.X;
src/main/java/u/U.java:package u;
src/main/java/u/U.java:import v.V;
src/main/java/u/U2.java:package u;
src/main/java/u/U2.java:import v.V;
src/main/java/v/V.java:package v;
src/main/java/v/V.java:import u.U;
src/main/java/k/K.java:package k;
src/main/java/k/K.java:import l.L;
src/main/java/l/L.java:package l;
src/main/java/l/L.java:import k.K;
src/main/java/l/L.java:import m.M;
src/main/java/l/L.java:import n.N;
src/main/java/m/M.java:package m;
src/main/java/m/M.java:import n.N;
src/main/java/n/N.java:package n;
src/main/java/n/N.java:import k.K;
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
......@@ -39,6 +39,7 @@
<module>osgi</module>
<module>rest</module>
<module>thirdparty</module>
<module>jdvue</module>
<module>jnc</module> <!-- FIXME publish and remove before release -->
</modules>
......