/** * Copyright (c) 2011 Cloudsmith 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: * Cloudsmith * */ package org.cloudsmith.geppetto.graph.dependency; import static org.cloudsmith.geppetto.forge.Forge.METADATA_JSON_NAME; import static org.cloudsmith.geppetto.forge.Forge.MODULEFILE_NAME; import static org.cloudsmith.geppetto.forge.v2.model.ModuleName.safeName; import static org.cloudsmith.geppetto.forge.v2.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.cloudsmith.geppetto.diagnostic.Diagnostic; import org.cloudsmith.geppetto.diagnostic.DiagnosticType; import org.cloudsmith.geppetto.forge.v2.model.Dependency; import org.cloudsmith.geppetto.forge.v2.model.Metadata; import org.cloudsmith.geppetto.forge.v2.model.ModuleName; import org.cloudsmith.geppetto.graph.DependencyGraphProducer; import org.cloudsmith.geppetto.graph.IHrefProducer; import org.cloudsmith.geppetto.pp.PPPackage; import org.cloudsmith.geppetto.pp.pptp.PPTPPackage; import org.cloudsmith.geppetto.semver.Version; import org.cloudsmith.geppetto.semver.VersionRange; import org.cloudsmith.geppetto.validation.runner.AllModuleReferences; import org.cloudsmith.geppetto.validation.runner.AllModuleReferences.Export; import org.cloudsmith.geppetto.validation.runner.BuildResult; import org.cloudsmith.geppetto.validation.runner.MetadataInfo; import org.cloudsmith.graph.ICancel; import org.cloudsmith.graph.ICancel.NullIndicator; import org.cloudsmith.graph.IVertex; import org.cloudsmith.graph.dot.DotRenderer; import org.cloudsmith.graph.elements.Edge; import org.cloudsmith.graph.elements.GraphElement; import org.cloudsmith.graph.elements.RootGraph; import org.cloudsmith.graph.elements.Vertex; import org.cloudsmith.graph.graphcss.GraphCSS; import org.cloudsmith.graph.graphcss.IFunctionFactory; import org.cloudsmith.graph.graphcss.StyleSet; import org.cloudsmith.graph.style.IStyle; import org.cloudsmith.graph.style.IStyleFactory; import org.cloudsmith.graph.style.labels.LabelRow; 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; /** * 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 = new ModuleName(safeOwner(parent.getName()), safeName(root.getName(), false), false); else moduleName = new ModuleName("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(new ModuleName("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 ? new ModuleName("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); } }