/**
* Copyright (c) 2013 Puppet Labs, Inc. and other contributors, as listed below.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Puppet Labs
*/
package com.puppetlabs.geppetto.graph.dependency;
import static com.puppetlabs.geppetto.forge.Forge.METADATA_JSON_NAME;
import static com.puppetlabs.geppetto.forge.Forge.MODULEFILE_NAME;
import static com.puppetlabs.geppetto.forge.model.ModuleName.safeName;
import static com.puppetlabs.geppetto.forge.model.ModuleName.safeOwner;
import java.io.File;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.emf.ecore.EClass;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.puppetlabs.geppetto.diagnostic.Diagnostic;
import com.puppetlabs.geppetto.diagnostic.DiagnosticType;
import com.puppetlabs.geppetto.forge.model.Dependency;
import com.puppetlabs.geppetto.forge.model.Metadata;
import com.puppetlabs.geppetto.forge.model.ModuleName;
import com.puppetlabs.geppetto.graph.DependencyGraphProducer;
import com.puppetlabs.geppetto.graph.IHrefProducer;
import com.puppetlabs.geppetto.pp.PPPackage;
import com.puppetlabs.geppetto.pp.pptp.PPTPPackage;
import com.puppetlabs.geppetto.semver.Version;
import com.puppetlabs.geppetto.semver.VersionRange;
import com.puppetlabs.geppetto.validation.runner.AllModuleReferences;
import com.puppetlabs.geppetto.validation.runner.AllModuleReferences.Export;
import com.puppetlabs.geppetto.validation.runner.BuildResult;
import com.puppetlabs.geppetto.validation.runner.MetadataInfo;
import com.puppetlabs.graph.ICancel;
import com.puppetlabs.graph.ICancel.NullIndicator;
import com.puppetlabs.graph.IVertex;
import com.puppetlabs.graph.dot.DotRenderer;
import com.puppetlabs.graph.elements.Edge;
import com.puppetlabs.graph.elements.GraphElement;
import com.puppetlabs.graph.elements.RootGraph;
import com.puppetlabs.graph.elements.Vertex;
import com.puppetlabs.graph.graphcss.GraphCSS;
import com.puppetlabs.graph.graphcss.IFunctionFactory;
import com.puppetlabs.graph.graphcss.StyleSet;
import com.puppetlabs.graph.style.IStyle;
import com.puppetlabs.graph.style.IStyleFactory;
import com.puppetlabs.graph.style.labels.LabelRow;
/**
* Calculates dependency data for a set of modules. The logic handles:
* <ul>
* <li>Existing and non existing modules</li>
* <li>Resolved dependencies (stated in Modulefiles)</li>
* <li>Unresolved dependencies (dependency on existing module, but unsatisfied version range).</li>
* <li>Unresolved dependencies to missing modules (all versions aggregated into one 'missing module')</li>
* <li>Implied 'dependencies' existing due to use of export from module</li>
* <li>Self references are included in the result</li>
* </ul>
* Each module is represented by an instance of {@link ModuleNodeData}, and each relationship is represented
* by an {@link ModuleEdge}. An edge is further described by an {@link EdgeType} as well as the implied meaning
* of the given set of non null fields. It is up to the user of this data to decide how to represent multiple
* edges, although it is suggested that a combination of resolved/implied is to be treated as "normal", and
* unresolved/implied as "unresolved", if a dependency was not stated there may be only "implied" edges.
*
* For all unresolved references (not found anywhere), the edge leads to a "null" {@link ModuleEdge}.
*
*/
public class DependencyDataCalculator implements DependencyGraphStyles, DependencyGraphProducer {
public static enum EdgeType {
UNRESOLVED, RESOLVED, IMPLIED;
}
public static class ModuleEdge {
final ModuleNodeData from;
final ModuleNodeData to;
final EdgeType edgeType;
final Iterable<Export> imported;
final Iterable<String> unresolved;
/**
* null if edgeType == IMPLIED
*/
Dependency dependency;
private Vertex vertex;
/**
* A (sub) collection of imported Exports that are ambiguous. May be null.
*/
final Collection<Export> ambiguities;
/**
* @param from
* @param implied
* @param unresolvedNames
*/
public ModuleEdge(ModuleNodeData from, EdgeType implied, Iterable<String> unresolvedNames) {
this(from, null, implied, null, null, unresolvedNames, null);
}
public ModuleEdge(ModuleNodeData from, ModuleNodeData to, EdgeType type, Dependency d) {
this(from, to, type, d, null, null, null);
}
public ModuleEdge(ModuleNodeData from, ModuleNodeData to, EdgeType type, Dependency d,
Iterable<Export> imported, Iterable<String> unresolved, Collection<Export> ambiguities) {
this.from = from;
this.to = to;
this.edgeType = type;
this.dependency = d;
this.imported = imported;
this.unresolved = unresolved;
this.ambiguities = ambiguities;
from.addOutgoing(to, this);
}
/**
* @param from
* @param to
* @param type
* @param imported
*/
public ModuleEdge(ModuleNodeData from, ModuleNodeData to, EdgeType type, Iterable<Export> imported,
Collection<Export> ambiguities) {
this(from, to, type, null, imported, null, ambiguities);
}
public Vertex getVertex() {
return vertex;
}
/**
* @param edgeVertex
*/
public void setVertex(Vertex edgeVertex) {
this.vertex = edgeVertex;
}
}
public static class ModuleNodeData {
public static ModuleNodeData existing(ModuleName name, Version version, boolean isNode, String href) {
return new ModuleNodeData(name, version, isNode
? ModuleType.PPNODE
: ModuleType.MODULE, href);
}
/**
* Formats the label of the node as [NameOf(root.parent)/]NameOf(root)
*
* @param root
* @return
*/
public static ModuleNodeData root(File root) {
File parent = root.getParentFile();
ModuleName moduleName;
if(parent != null)
moduleName = ModuleName.create(safeOwner(parent.getName()), safeName(root.getName(), false), false);
else
moduleName = ModuleName.create("root", ModuleName.safeName(root.getName(), false), false);
return new ModuleNodeData(moduleName, null, ModuleType.ROOT, "");
}
public static ModuleNodeData unresolved(ModuleName name) {
return new ModuleNodeData(name, null, ModuleType.NON_EXISTING, "");
}
IVertex vertex;
ModuleName name;
Version version;
ModuleType moduleType;
String href;
boolean marked;
Multimap<ModuleNodeData, ModuleEdge> outgoing;
private int ambiguous;
private ModuleNodeData(ModuleName name, Version version, ModuleType type, String href) {
this.name = name;
this.version = version;
this.moduleType = type;
this.outgoing = ArrayListMultimap.create();
this.href = href;
this.ambiguous = 0;
}
private void addOutgoing(ModuleNodeData to, ModuleEdge edge) {
// filter out direct self references
if(to == this)
return;
outgoing.put(to, edge);
}
public boolean exists() {
return moduleType != ModuleType.NON_EXISTING;
}
private IVertex getVertex() {
return vertex;
}
public boolean isAmbiguous() {
return this.ambiguous > 0;
}
public boolean isNode() {
return moduleType == ModuleType.PPNODE;
}
/**
* mark all reachable module nodes.
*/
public void mark() {
if(marked)
return;
marked = true;
for(ModuleNodeData reachable : outgoing.keySet())
if(reachable != null) // i.e. if resolved
reachable.mark();
}
/**
* @param b
*/
public void setAmbiguous(int counter) {
this.ambiguous = counter;
}
private void setVertex(IVertex v) {
vertex = v;
}
}
public static enum ModuleType {
ROOT, PPNODE, MODULE, NON_EXISTING, PPTP;
}
@Inject
private IStyleFactory styles;
@Inject
private DependencyGraphTheme theme;
@Inject
private DotRenderer dotRenderer;
@Inject
private IHrefProducer hrefProducer;
@Inject
private GraphCSS instanceRules;
private Map<Object, ModuleNodeData> moduleNodeData = Maps.newHashMap();
private List<ModuleEdge> moduleEdges = Lists.newArrayList();
private ModuleNodeData nonModularNode;
private ModuleNodeData pptpNode;
private File root;
public static final DiagnosticType DEPENDENCY_DATA_CALCULATOR = new DiagnosticType(
"DEPENDENCY_DATA_CALCULATOR", DependencyDataCalculator.class.getName());
private ModuleNodeData _file2Module(File f, Map<File, ModuleNodeData> index) {
String path = f.getPath();
ModuleNodeData md;
if(path.endsWith(MODULEFILE_NAME) || path.endsWith(METADATA_JSON_NAME))
md = index.get(f);
else {
md = index.get(new File(path + '/' + MODULEFILE_NAME));
if(md == null)
md = index.get(new File(path + '/' + METADATA_JSON_NAME));
}
return md;
}
private void addEdgeHref(ModuleNodeData a, ModuleNodeData b, GraphElement... elements) {
String aId = idOfVertex(a);
String hrefForEdge = null;
String bId = b == null || b == pptpNode
? ""
: idOfVertex(b);
boolean splitEdge = elements.length > 1;
if(b == null)
hrefForEdge = hrefProducer.hrefForEdgeToUnresolved(aId, splitEdge);
else if(b == pptpNode)
hrefForEdge = hrefProducer.hrefForEdgeToPptp(aId);
else
hrefForEdge = hrefProducer.hrefForEdge(aId, bId, splitEdge);
IStyle<String> hrefStyle = styles.href(hrefForEdge);
for(GraphElement e : elements)
e.setStyles(StyleSet.withStyle(hrefStyle));
}
private void addStyleClasses(List<String> classes, GraphElement... elements) {
for(GraphElement e : elements)
e.addAllStyleClasses(classes);
}
/**
* Install tooltip as instance style for edge.
*
* @param tooltip
* @param edges
*/
private void addTooltip(String tooltip, GraphElement... elements) {
for(GraphElement e : elements)
e.setStyles(StyleSet.withStyle(styles.tooltip(tooltip)));
}
/**
* Calculates dependency data and returns a map from Modulefiles to ModuleNodeData.
*
* @param moduleData
* @param exportData
* @return
*/
public Map<File, ModuleNodeData> calculateDependencyData(File root, Multimap<ModuleName, MetadataInfo> moduleData,
AllModuleReferences exportData) {
// create node data for all existing modules and check if there are ambiguities
Multimap<ModuleName, ModuleNodeData> processedModules = ArrayListMultimap.create();
for(MetadataInfo mi : moduleData.values()) {
Metadata m = mi.getMetadata();
ModuleNodeData mnd = ModuleNodeData.existing(m.getName(), m.getVersion(), mi.isRole(), toHREF_URL(mi));
moduleNodeData.put(mi, mnd);
processedModules.put(m.getName(), mnd);
}
for(ModuleName key : processedModules.keySet()) {
Collection<ModuleNodeData> modules = processedModules.get(key);
if(modules.size() > 1) {
int counter = 0;
for(ModuleNodeData mnd : modules)
mnd.setAmbiguous(++counter);
}
}
// moduleData is keyed by "fullName" to lower case
// Create pseudo module for non modular content
nonModularNode = ModuleNodeData.root(root);
pptpNode = new ModuleNodeData(ModuleName.create("root", "puppet", false), null, ModuleType.PPTP, ""); // will not be rendered
// create module nodes for missing (unsatisfied dependencies)
// unless dependency is to represented module name, but version is not matched (in which case
// the unmatched but existing node is used.
// if a dependency appears more than once, use the first (skip the rest with same name)
for(MetadataInfo mi : moduleData.values()) {
final ModuleNodeData a = moduleNodeData.get(mi);
Set<ModuleName> processed = Sets.newHashSet();
for(Dependency d : mi.getUnresolvedDependencies()) {
final ModuleName name = d.getName();
if(!processed.add(name))
continue;
Collection<MetadataInfo> existingVersions = moduleData.get(name);
ModuleNodeData b = null;
if(existingVersions == null || existingVersions.size() < 1) {
b = moduleNodeData.get(name);
if(b == null) {
// need a node for the missing module
b = ModuleNodeData.unresolved(name);
// need to generate one that can not be found if name is null
moduleNodeData.put(name == null
? ModuleName.create("no", "name", false)
: name, b);
}
}
else {
// pick (one of) the existing versions (it is actually illegal to have more
// than one, so just pick the first one).
MetadataInfo first = Iterables.get(existingVersions, 0);
b = moduleNodeData.get(first);
}
createUnresolvedEdge(a, b, d);
}
// Add edges for all resolved dependencies
for(MetadataInfo.Resolution r : mi.getResolvedDependencies()) {
createResolvedEdge(a, moduleNodeData.get(r.metadata), r.dependency);
}
}
Map<File, ModuleNodeData> fileIndex = Maps.newHashMap();
for(Map.Entry<Object, ModuleNodeData> m : moduleNodeData.entrySet()) {
if(!(m.getKey() instanceof ModuleName)) {
MetadataInfo mi = (MetadataInfo) m.getKey();
fileIndex.put(mi.getFile(), m.getValue());
}
}
Map<File, Multimap<File, Export>> ambiguities = exportData.getAmbiguityMap();
for(Map.Entry<File, Multimap<File, AllModuleReferences.Export>> x : exportData.getImportMap().entrySet()) {
// get the imported
File fromFile = x.getKey();
Multimap<File, Export> m = x.getValue();
// get any ambiguities
Multimap<File, Export> ambiguitiesForFile = ambiguities.get(fromFile);
for(File toFile : m.keySet()) {
createImportEdge(
file2Module(fromFile, fileIndex), file2Module(toFile, fileIndex), m.get(toFile),
ambiguitiesForFile != null
? ambiguitiesForFile.get(toFile)
: null);
}
}
for(File fromFile : exportData.getUnresolvedMap().keySet()) {
createUnresolvedEdge(file2Module(fromFile, fileIndex), exportData.getUnresolvedMap().get(fromFile));
}
return fileIndex;
}
private List<String> classesFor(ModuleNodeData from, ModuleNodeData to) {
List<String> styleClasses = Lists.newArrayList();
if(from != null)
styleClasses.add("FROM__" + idOfVertex(from));
if(to != null && to != pptpNode)
styleClasses.add("TO__" + idOfVertex(to));
return styleClasses;
}
private List<String> classesOfEdge(ModuleEdge edge) {
return classesFor(edge.from, edge.to);
}
private void createImportEdge(ModuleNodeData from, ModuleNodeData to, Iterable<Export> imported,
Collection<Export> ambiguities) {
moduleEdges.add(new ModuleEdge(from, to, EdgeType.IMPLIED, imported, ambiguities));
}
private void createResolvedEdge(final ModuleNodeData a, final ModuleNodeData b, Dependency d) {
moduleEdges.add(new ModuleEdge(a, b, EdgeType.RESOLVED, d));
}
private void createUnresolvedEdge(ModuleNodeData from, Iterable<String> unresolvedNames) {
moduleEdges.add(new ModuleEdge(from, EdgeType.IMPLIED, unresolvedNames));
}
private void createUnresolvedEdge(final ModuleNodeData a, final ModuleNodeData b, Dependency d) {
moduleEdges.add(new ModuleEdge(a, b, EdgeType.UNRESOLVED, d));
}
private Vertex createVertexForEdge(ModuleEdge me) {
if(me.imported == null && me.unresolved == null)
// if neither imported nor unresolved, a vertex is not needed
return null;
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("References from ");
stringBuilder.append(makeTooltip(me.from, me.to));
String tooltipString = stringBuilder.toString();
Vertex edgeVertex = null;
if(me.imported != null) {
edgeVertex = new Vertex("", STYLE_CLASS_IMPORTS);
edgeVertex.setStyles(labelStyleForImported(me.imported, me.ambiguities).add(
StyleSet.withStyle(styles.tooltip(tooltipString))));
me.setVertex(edgeVertex);
}
else {
edgeVertex = new Vertex("", STYLE_CLASS_UNRESOLVED_IMPORTS);
edgeVertex.setStyles(labelStyleForUnresolved(me).add(StyleSet.withStyle(styles.tooltip(tooltipString))));
me.setVertex(edgeVertex);
}
// Set style classes for FROM and TO
edgeVertex.addAllStyleClasses(classesOfEdge(me));
return edgeVertex;
}
private Vertex createVertexForModuleNode(ModuleNodeData mnd) {
StringBuilder builder = new StringBuilder();
builder.append(mnd.name);
if(mnd.isAmbiguous()) {
builder.append(" [");
builder.append(mnd.ambiguous);
builder.append("]");
}
if(mnd.version != null) {
builder.append("\n");
builder.append(mnd.version);
}
String label = builder.toString();
String style = mnd.exists()
? (mnd.isAmbiguous())
? STYLE_CLASS_AMBIGUOUSLY_RESOLVED_MODULE
: STYLE_CLASS_RESOLVED_MODULE
: STYLE_CLASS_UNRESOLVED_MODULE;
Vertex v = new Vertex(label, style);
v.setStyles(styles.href(mnd.href));
v.putUserData(IFunctionFactory.ID_KEY, idOfVertex(mnd));
mnd.setVertex(v);
return v;
}
private Vertex createVertexForPPNodeNode(ModuleNodeData mnd) {
String label = mnd.name == null
? ""
: mnd.name.toString();
// no version (user is not aware of one).
String style = STYLE_CLASS_PPNODE_MODULE;
Vertex v = new Vertex(label, style);
v.setStyles(styles.href(mnd.href));
v.putUserData(IFunctionFactory.ID_KEY, idOfVertex(mnd));
mnd.setVertex(v);
return v;
}
private Vertex createVertexForRootNode(ModuleNodeData mnd) {
String label = mnd.name.toString();
if(mnd.version != null)
label += "\n" + mnd.version;
Vertex v = new Vertex(label, STYLE_CLASS_ROOT);
mnd.setVertex(v);
return v;
}
protected void dumpInfo() {
for(ModuleNodeData m : moduleNodeData.values()) {
System.err.printf("module %s(%s) = %s\n", m.name, m.version, m.exists()
? "exists"
: "missing");
for(ModuleNodeData to : m.outgoing.keySet()) {
System.err.printf(" -> %s\n", to != null
? to.name
: "null 'to'");
for(ModuleEdge edge : m.outgoing.get(to)) {
System.err.printf(" %s, ", edge.edgeType);
if(edge.imported != null)
for(Export e : edge.imported)
System.err.printf("%s ", e.getName());
if(edge.unresolved != null)
for(String s : edge.unresolved)
System.err.printf("(unresolved) %s ", s);
if(edge.imported != null || edge.unresolved != null)
System.err.println();
if(edge.dependency != null)
if(edge.dependency.getVersionRequirement() == null)
System.err.println(">= 0");
else
System.err.printf("%s\n", edge.dependency.getVersionRequirement());
}
}
}
}
private ModuleNodeData file2Module(File f, Map<File, ModuleNodeData> index) {
if(f.getPath().equals("_pptp"))
return pptpNode;
ModuleNodeData mnd = _file2Module(f, index);
return mnd != null
? mnd
: nonModularNode;
}
public String getVersionLabel(ModuleEdge edge) {
VersionRange vreq = edge.dependency.getVersionRequirement();
if(vreq == null)
return "unversioned";
return vreq.toString();
}
/**
* Produce id on the form NAME[@ambiguityIdx] e.g. Foo, Bar-1, Bar-2
*
* @param mnd
* @return
*/
private String idOfVertex(ModuleNodeData mnd) {
StringBuilder idBuilder = new StringBuilder();
idBuilder.append(mnd.name);
if(mnd.isAmbiguous()) {
idBuilder.append("__");
idBuilder.append(mnd.ambiguous);
}
return idBuilder.toString();
}
private String labelOfType(EClass clazz) {
if(clazz == PPPackage.Literals.DEFINITION)
return "define";
if(clazz == PPPackage.Literals.HOST_CLASS_DEFINITION)
return "class";
if(clazz == PPPackage.Literals.NODE_DEFINITION)
return "node";
if(clazz == PPTPPackage.Literals.FUNCTION)
return "function";
if(clazz == PPTPPackage.Literals.TYPE)
return "type";
return clazz.getName(); // give up, but output something (should not really happen)
}
private StyleSet labelStyleForImported(Iterable<Export> imports, Collection<Export> ambiguities) {
List<LabelRow> labelRows = Lists.newArrayList();
Export[] sortedImports = Iterables.toArray(imports, Export.class);
Arrays.sort(sortedImports, new Comparator<Export>() {
@Override
public int compare(Export o1, Export o2) {
// they are not supposed to be null, but just to make sure we don't get a NPE
// because of a bug elsewhere
if(o1 == null || o2 == null)
return 0;
return o1.getName().compareTo(o2.getName());
}
});
for(Export a : sortedImports) {
if(a == null) {
System.err.println("Null Export found");
continue;
}
final boolean ambiguous = ambiguities == null
? false
: ambiguities.contains(a);
labelRows.add(styles.labelRow(STYLE__IMPORT_ROW, //
styles.labelCell( //
ambiguous
? STYLE__IMPORT_AMBIGUOUS_NAME_CELL
: STYLE__IMPORT_NAME_CELL, //
a.getName()).withStyle(//
styles.href(toHREF_URL(a))), //
styles.labelCell(STYLE__IMPORT_TYPE_CELL, labelOfType(a.getEClass()))));
}
return StyleSet.withStyle(styles.labelFormat(styles.labelTable(
STYLE__IMPORT_TABLE, labelRows.toArray(new LabelRow[0]))));
}
private StyleSet labelStyleForUnresolved(ModuleEdge me) {
Iterable<String> unresolved = me.unresolved;
List<LabelRow> labelRows = Lists.newArrayList();
String[] sortedImports = Iterables.toArray(unresolved, String.class);
Arrays.sort(sortedImports, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
for(String a : sortedImports) {
labelRows.add(styles.labelRow(STYLE__UNRESOLVED_ROW, //
styles.labelCell(STYLE__UNRESOLVED_NAME_CELL, a).withStyle( //
styles.href(toHREF_UNRESOLVED(me.from.name, a))) //
));
}
return StyleSet.withStyle(styles.labelFormat(styles.labelTable(
STYLE__IMPORT_TABLE, labelRows.toArray(new LabelRow[0]))));
}
/**
* @param a
* @param b
* @return
*/
private String makeTooltip(ModuleNodeData a, ModuleNodeData b) {
final StringBuilder tooltipBuilder = new StringBuilder();
tooltipBuilder.append(a.name);
tooltipBuilder.append(" ⇒ "); // i.e. ⇒ a non defined entity in SVG
if(b != null)
tooltipBuilder.append(b.name);
return tooltipBuilder.toString();
}
public void produceGraph(ICancel cancel, String title, File[] roots, OutputStream output, BuildResult buildResult,
Diagnostic chain) {
if(title == null)
title = "Module Dependencies";
AllModuleReferences all = buildResult.getAllModuleReferences();
produceGraph(cancel, title, roots, output, all.getRoot(), buildResult.getModuleData(), all, chain);
}
/**
*
* @param root
* @param moduleData
* Name -> 0* MetadataInfo representing one version of a module with given name
*/
public void produceGraph(ICancel cancel, String title, File[] roots, OutputStream output, File root,
Multimap<ModuleName, MetadataInfo> moduleData, AllModuleReferences exportData, Diagnostic chain) {
if(cancel == null)
cancel = new NullIndicator();
this.root = root;
Map<File, ModuleNodeData> fileMap = calculateDependencyData(root, moduleData, exportData);
// Render all unless there is a list of roots to include. If roots are given, mark their
// transitive closures, and tell renderer to render only those that are marked.
boolean renderAll = roots == null || roots.length == 0;
if(!renderAll)
for(File f : roots) {
ModuleNodeData x = file2Module(f, fileMap);
if(x != null)
x.mark();
}
else {
// mark all
for(ModuleNodeData x : fileMap.values())
x.mark();
}
RootGraph g = produceRootGraph(cancel, title, moduleData, exportData, renderAll, chain);
instanceRules.addAll(theme.getInstanceRules());
dotRenderer.write(cancel, output, g, theme.getDefaultRules(), instanceRules);
}
/**
* Produces the graph with vertex and edge data. If renderAll is false, only marked ModuleNodeData
* will be rendered.
*
* @param title
* @param moduleData
* @param exportData
* @param renderAll
* @return
*/
private RootGraph produceRootGraph(ICancel cancel, String title, Multimap<ModuleName, MetadataInfo> moduleData,
AllModuleReferences exportData, boolean renderAll, Diagnostic chain) {
if(title == null)
title = "";
if(cancel == null)
cancel = new NullIndicator();
RootGraph g = new RootGraph(title, "RootGraph", "root");
// Create Graph vertexes, one per existing module
// and one for the root/non-modular
for(ModuleNodeData mnd : moduleNodeData.values()) {
cancel.assertContinue();
if(renderAll || mnd.marked)
if(mnd.isNode())
g.addVertex(createVertexForPPNodeNode(mnd));
else
g.addVertex(createVertexForModuleNode(mnd));
}
// only draw the root node if it has been marked (incoming dependency), or has outgoing
// when not rendering all, the non modular must have incoming dependencies to show.
if(renderAll && nonModularNode.outgoing.size() > 0 || nonModularNode.marked) {
Vertex v = createVertexForRootNode(nonModularNode);
g.addVertex(v);
}
// Create Vertexes for all Edges that have data
// (very large labels on edges does not work that well as they often overlap)
// Skip edges that are from non marked module nodes unless everything is rendered
for(ModuleEdge me : moduleEdges) {
// filter out self references
if(me.from == me.to)
continue;
if(renderAll || me.from.marked) {
cancel.assertContinue();
Vertex v = createVertexForEdge(me);
if(v != null) {
g.addVertex(v);
}
}
}
// Create Edges
// If an edge has a vertex, it needs to be drawn as two separate graph edges
for(ModuleNodeData a : Iterables.concat(moduleNodeData.values(), Collections.singleton(nonModularNode))) {
if(!(renderAll || a.marked))
continue; // skip non marked unless all is rendered
for(ModuleNodeData b : a.outgoing.keySet()) {
cancel.assertContinue();
int resolved = 0;
int unresolved = 0;
int implied = 0;
int count = 0;
List<ModuleEdge> edges = new ArrayList<ModuleEdge>();
Collection<ModuleEdge> outgoing = a.outgoing.get(b);
for(ModuleEdge e : outgoing) {
edges.add(e);
switch(e.edgeType) {
case IMPLIED:
implied++;
break;
case UNRESOLVED:
unresolved++;
break;
case RESOLVED:
resolved++;
break;
default:
throw new IllegalStateException("Illegal edge type found");
}
++count;
}
String tooltipString = makeTooltip(a, b);
List<String> styleClasses = classesFor(a, b);
// CASE 1
// A --> [...] --> B
// A imports from resolved B
if(count == 2 && resolved == 1 && implied == 1) {
Edge e1 = new Edge("", STYLE_EDGE__IMPORT, a.getVertex(), edges.get(1).getVertex());
Edge e2 = new Edge(getVersionLabel(edges.get(0)), STYLE_EDGE__RESOLVED_DEP, //
edges.get(1).getVertex(), b.getVertex());
g.addEdge(e1, e2);
addTooltip(tooltipString, e1, e2);
addStyleClasses(styleClasses, e1, e2);
addEdgeHref(a, b, e1, e2);
}
else if(count == 1 && implied == 1) {
// CASE 5
// A --> [...]
if(edges.get(0).to == null) {
Edge e1 = new Edge("unresolved", STYLE_EDGE__UIMPORT, a.getVertex(), edges.get(0).getVertex());
g.addEdge(e1);
addTooltip(tooltipString, e1);
addStyleClasses(styleClasses, e1);
addEdgeHref(a, b, e1);
}
// CASE 8 (reference to pptp - do not draw imports -> pptp part)
// A -->[...]
else if(edges.get(0).to == pptpNode) {
Edge e1 = new Edge("puppet", STYLE_EDGE__IMPORT, a.getVertex(), edges.get(0).getVertex());
g.addEdge(e1);
// default tooltip == label
addStyleClasses(styleClasses, e1);
addEdgeHref(a, b, e1);
}
// CASE 2 (and CASE 0 - self reference)
// A -->[...] ~~> B
else {
if(edges.get(0).from != edges.get(0).to) { // skip self references
Edge e1 = new Edge("", STYLE_EDGE__IMPORT, a.getVertex(), edges.get(0).getVertex());
Edge e2 = new Edge(
"implicit", STYLE_EDGE__IMPLIED_DEP, edges.get(0).getVertex(),
edges.get(0).to.getVertex());
g.addEdge(e1, e2);
addTooltip(tooltipString, e1, e2);
addStyleClasses(styleClasses, e1, e2);
addEdgeHref(a, b, e1, e2);
}
}
}
// CASE 3
// A --> [...] ~~>(not in range) B
else if(count == 2 && implied == 1 && unresolved == 1) {
Edge e1 = new Edge("", STYLE_EDGE__IMPORT, a.getVertex(), edges.get(1).getVertex());
String label = "implicit\\nunresolved\\n" + getVersionLabel(edges.get(0));
Edge e2 = new Edge(label, STYLE_EDGE__UNRESOLVED_IMPLIED_DEP, //
edges.get(1).getVertex(), edges.get(1).to.getVertex());
g.addEdge(e1, e2);
addTooltip(tooltipString, e1, e2);
addStyleClasses(styleClasses, e1, e2);
addEdgeHref(a, b, e1, e2);
}
else if(count == 1 && unresolved == 1) {
// CASE 7
// A ~~> B where B is not in range
if(edges.get(0).to.exists()) {
String label = "unresolved\\n" + getVersionLabel(edges.get(0));
Edge e1 = new Edge(label, STYLE_EDGE__UNRESOLVED_IMPLIED_DEP, //
a.getVertex(), b.getVertex());
g.addEdge(e1);
addTooltip(tooltipString, e1);
addStyleClasses(styleClasses, e1);
addEdgeHref(a, b, e1);
}
// CASE 4
// A --> B where B does not exist
else {
String label = "unresolved\\n" + getVersionLabel(edges.get(0));
Edge e1 = new Edge(label, STYLE_EDGE__UNRESOLVED_DEP, //
a.getVertex(), b.getVertex());
g.addEdge(e1);
addTooltip(tooltipString, e1);
addStyleClasses(styleClasses, e1);
addEdgeHref(a, b, e1);
}
}
// CASE 6
// A --> B
// (nothing is imported from B)
else if(count == 1 && resolved == 1) {
Edge e1 = new Edge(getVersionLabel(edges.get(0)), STYLE_EDGE__RESOLVED_DEP, //
a.getVertex(), b.getVertex());
g.addEdge(e1);
addTooltip(tooltipString, e1);
addStyleClasses(styleClasses, e1);
addEdgeHref(a, b, e1);
}
else if(count == implied) {
// TODO: Figure out what this means. It happens when importing puppetlabs/stdlib into
// cloudsmith/wordpress:
//
// a=thallgren/wordpress
// b=root/puppet
}
else {
// StringBuilder builder = new StringBuilder();
// builder.append("Internal Error - illegal combination of recorded edges");
// builder.append(" count=");
// builder.append(count);
// builder.append(" resolved=");
// builder.append(resolved);
// builder.append(" unresolved=");
// builder.append(unresolved);
// builder.append(" a =");
// builder.append(a.name);
// builder.append(" b=");
// builder.append(b == null
// ? "implied"
// : b.name);
// chain.addChild(new Diagnostic(Diagnostic.ERROR, DEPENDENCY_DATA_CALCULATOR, builder.toString()));
}
}
}
return g;
}
public String toHREF_UNRESOLVED(ModuleName fromModuleName, String name) {
return hrefProducer.hrefForUnresolved(fromModuleName, name);
}
private String toHREF_URL(Export e) {
return hrefProducer.href(e, this.root);
}
public String toHREF_URL(MetadataInfo mi) {
return hrefProducer.href(mi, this.root);
}
}