/** * 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.catalog; import java.io.OutputStream; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import org.cloudsmith.geppetto.catalog.Catalog; import org.cloudsmith.geppetto.catalog.CatalogEdge; import org.cloudsmith.geppetto.catalog.CatalogResource; import org.cloudsmith.geppetto.catalog.CatalogResourceParameter; import org.cloudsmith.graph.ICancel; import org.cloudsmith.graph.IGraphElement; import org.cloudsmith.graph.ILabeledGraphElement; import org.cloudsmith.graph.elements.Edge; import org.cloudsmith.graph.elements.RootGraph; import org.cloudsmith.graph.elements.Vertex; import org.cloudsmith.graph.graphcss.StyleSet; import org.cloudsmith.graph.style.Span; import org.cloudsmith.graph.style.labels.ILabelTemplate; import org.cloudsmith.graph.style.labels.LabelCell; import org.cloudsmith.graph.style.labels.LabelRow; import org.cloudsmith.graph.style.labels.LabelStringTemplate; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import com.google.common.base.Function; import com.google.common.base.Predicates; import com.google.common.collect.HashMultimap; 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.common.collect.Sets.SetView; /** * Produces a Catalog graph in DOT format. * */ public class CatalogDeltaGraphProducer extends AbstractCatalogGraphProducer implements CatalogGraphStyles { private static class MarkerFunction implements Function<IGraphElement, ILabelTemplate> { private String marker; MarkerFunction(String marker) { this.marker = marker; } @Override public ILabelTemplate apply(IGraphElement from) { String text = ""; if(from instanceof ILabeledGraphElement) { ILabeledGraphElement labeled = (ILabeledGraphElement) from; String label = labeled.getLabel(); if(label.length() < 1) text = marker; else { StringBuilder builder = new StringBuilder(); builder.append(marker); builder.append(" "); builder.append(label); text = builder.toString(); } } return new LabelStringTemplate(text); } } private static class PropertyDeltaInfo { int modifiedCount; int width; String singleResourceStyle; PropertyDeltaInfo() { modifiedCount = 0; width = 0; singleResourceStyle = null; } } private static final String GT = ">"; private static final String LT = "<"; private static Function<IGraphElement, ILabelTemplate> markRemoved = new MarkerFunction(LT); private static Function<IGraphElement, ILabelTemplate> markAdded = new MarkerFunction(GT); private PropertyDeltaInfo computePropertyRows(CatalogResource oldR, CatalogResource newR, List<LabelRow> labelRows) { final PropertyDeltaInfo result = new PropertyDeltaInfo(); final Function<IGraphElement, Boolean> renderMarkerColumnFunc = new Function<IGraphElement, Boolean>() { @Override public Boolean apply(IGraphElement from) { return result.modifiedCount > 0; } }; Multimap<String, String> properties = HashMultimap.create(); Map<String, String> oldProperties = Maps.newHashMap(); Map<String, String> newProperties = Maps.newHashMap(); Map<String, String> propertiesToUse = null; if(oldR == null || newR == null) { if(oldR == null) { result.singleResourceStyle = STYLE_Added; propertiesToUse = newProperties; } else { result.singleResourceStyle = STYLE_Removed; propertiesToUse = oldProperties; } } if(oldR != null) for(CatalogResourceParameter p : Iterables.filter(getParameterIterable(oldR), regularParameterPredicate)) { final String name = p.getName(); final String value = stringify(p.getValue()); properties.put(name, value); oldProperties.put(name, value); } if(newR != null) for(CatalogResourceParameter p : Iterables.filter(getParameterIterable(newR), regularParameterPredicate)) { final String name = p.getName(); final String value = stringify(p.getValue()); properties.put(name, value); newProperties.put(name, value); } // Sort the keys Set<String> sortedNames = Sets.newTreeSet(); sortedNames.addAll(properties.keySet()); for(String propertyName : sortedNames) { Collection<String> values = properties.get(propertyName); String styleClass = null; // if two different values (can only happen if two resources were given) if(values.size() > 1) { styleClass = STYLE_Modified; String valueOld = oldProperties.get(propertyName); labelRows.add(getStyles().labelRow( STYLE_ResourcePropertyRow, // createResourcePropertyMarker(LT, STYLE_Removed, renderMarkerColumnFunc), // getStyles().labelCell( Sets.newHashSet(STYLE_ResourcePropertyName, STYLE_Removed), propertyName, Span.rowSpan(1)), // getStyles().labelCell( Sets.newHashSet(STYLE_ResourcePropertyValue, STYLE_Removed), DOUBLE_RIGHT_ARROW + // valueOld, Span.colSpan(1))// )); String valueNew = newProperties.get(propertyName); labelRows.add(getStyles().labelRow( STYLE_ResourcePropertyRow, // createResourcePropertyMarker(GT, STYLE_Added, renderMarkerColumnFunc), // getStyles().labelCell( Sets.newHashSet(STYLE_ResourcePropertyName, STYLE_Added), propertyName, Span.rowSpan(1)), // getStyles().labelCell( Sets.newHashSet(STYLE_ResourcePropertyValue, STYLE_Added), DOUBLE_RIGHT_ARROW + // valueNew, Span.colSpan(1)) // )); result.width = Math.max( result.width, propertyName.length() + Math.max(valueOld.length(), valueNew.length()) + 2); } else { // added, removed, or unmodified String value = null; String marker = ""; if(result.singleResourceStyle == null) { boolean inOld = oldProperties.containsKey(propertyName); boolean inNew = newR != null && newProperties.containsKey(propertyName); if(inOld && inNew) { styleClass = STYLE_UnModified; value = oldProperties.get(propertyName); } else if(inNew) { styleClass = STYLE_Added; value = newProperties.get(propertyName); result.modifiedCount++; marker = GT; } else if(inOld) { styleClass = STYLE_Removed; value = oldProperties.get(propertyName); result.modifiedCount++; marker = LT; } } else { styleClass = result.singleResourceStyle; value = propertiesToUse.get(propertyName); } // Can not output the entire file content as the value, simply use "DATA" if("content".equals(propertyName)) value = "DATA"; labelRows.add(getStyles().labelRow(STYLE_ResourcePropertyRow, // createResourcePropertyMarker(marker, styleClass, renderMarkerColumnFunc), // getStyles().labelCell(Sets.newHashSet(STYLE_ResourcePropertyName, styleClass), propertyName), // getStyles().labelCell(Sets.newHashSet(STYLE_ResourcePropertyValue, styleClass), DOUBLE_RIGHT_ARROW + // value, Span.colSpan(1)) // )); result.width = Math.max(result.width, propertyName.length() + value.length() + 2); } } return result; } private LabelCell createResourcePropertyMarker(String marker, String styleName, Function<IGraphElement, Boolean> renderedFunc) { return getStyles().labelCell(Sets.newHashSet(STYLE_ResourcePropertyMarker, styleName), marker)// .withStyles(// getStyles().fixedSize(true), getStyles().fixedSize(true), getStyles().width(8), getStyles().height(8), getStyles().cellSpacing(0), // getStyles().cellPadding(0), getStyles().rendered(renderedFunc) // ); } private void createVertexesFor(Iterable<CatalogResource> resources, // Map<String, Vertex> vertexMap, // Map<CatalogResource, Vertex> resourceVertexMap, // Map<Vertex, CatalogResource> catalogMap) { for(CatalogResource r : resources) { Vertex v = createVertexFor(r); vertexMap.put(keyOf(r), v); resourceVertexMap.put(r, v); catalogMap.put(v, r); } } private Vertex createVertexFor(CatalogResource resource) { Vertex v = new Vertex("", STYLE_Resource); return v; } private void edgesForCatalogEdges(Iterable<CatalogEdge> catalogEdges, // Map<String, Vertex> vertexMap, // Map<String, Edge> edgeMap, // Set<String> edges) { for(CatalogEdge e : catalogEdges) { Vertex source = vertexMap.get(e.getSource().toLowerCase()); Vertex target = vertexMap.get(e.getTarget().toLowerCase()); Edge edge = new Edge("", STYLE_ResourceEdge, source, target); final String key = keyOf(e); edgeMap.put(key, edge); edges.add(key); } } private void edgesForResources(RootGraph g, Iterable<CatalogResource> resources, Map<CatalogResource, Vertex> resourceVertexMap, // Map<String, Vertex> vertexMap, // Map<String, Edge> edgeMap, // Set<String> edges) { for(CatalogResource r : resources) { final String sourceKey = keyOf(r); final Vertex source = vertexMap.get(sourceKey); for(CatalogResourceParameter p : Iterables.filter( getParameterIterable(r), Predicates.not(regularParameterPredicate))) { String aName = p.getName(); String style = null; if("subscribe".equals(aName)) style = CatalogGraphStyles.STYLE_SubscribeEdge; else if("before".equals(aName)) style = CatalogGraphStyles.STYLE_BeforeEdge; else if("notify".equals(aName)) style = CatalogGraphStyles.STYLE_NotifyEdge; else style = CatalogGraphStyles.STYLE_RequireEdge; for(String targetRef : p.getValue()) { final String targetKey = targetRef.toLowerCase(); Vertex target = vertexMap.get(targetKey); if(target == null) { target = createVertexForMissingResource(targetRef); vertexMap.put(targetKey, target); // keep it if there are more references g.addVertex(target); } Edge edge = new Edge(aName, style, source, target); String key = sourceKey + "-" + aName + "-" + targetKey; edgeMap.put(key, edge); edges.add(key); } } } } private void generateEdgeDelta(RootGraph g, Set<String> edges, Map<String, Edge> oldEdgeMap, Map<String, Edge> newEdgeMap) { for(String key : edges) { boolean inOld = oldEdgeMap.containsKey(key); boolean inNew = newEdgeMap.containsKey(key); Edge e = null; if(inOld && inNew) { e = oldEdgeMap.get(key); e.addStyleClass(STYLE_UnModified); } else if(inOld) { e = oldEdgeMap.get(key); e.addStyleClass(STYLE_Removed); e.setStyles(getStyles().labelFormat(getStyles().labelTemplate(markRemoved))); } else { e = newEdgeMap.get(key); e.addStyleClass(STYLE_Added); e.setStyles(getStyles().labelFormat(getStyles().labelTemplate(markAdded))); } g.addEdge(e); } } private String keyOf(CatalogEdge e) { return e.getSource().toLowerCase() + "-" + e.getTarget().toLowerCase(); } private String keyOf(CatalogResource r) { StringBuilder builder = new StringBuilder(); builder.append(r.getType().toLowerCase()); builder.append("["); builder.append(r.getTitle().toLowerCase()); builder.append("]"); return builder.toString(); } private StyleSet labelStyleForResource(CatalogResource oldR, IPath oldRoot, CatalogResource newR, IPath newRoot, String[] resultingStyle) { if(resultingStyle == null || resultingStyle.length != 1) throw new IllegalArgumentException("resulting style must be String[1]"); final CatalogResource singleResource = newR == null ? oldR : newR; final IPath singleRoot = newR == null ? oldRoot : newRoot; if(singleResource == null) throw new IllegalArgumentException("At least one catalog must be specified"); if(oldR != null && newR != null) { if(!(oldR.getType().equals(newR.getType()) && oldR.getTitle().toLowerCase().equals( newR.getTitle().toLowerCase()))) throw new IllegalArgumentException("old and new resource must have same type and title"); } // PROPERTIES List<LabelRow> innerLabelRows = Lists.newArrayList(); final PropertyDeltaInfo propertyInfo = computePropertyRows(oldR, newR, innerLabelRows); int width = propertyInfo.width; // RESULTING OVERALL STYLE // i.e. if only one resource - it is either added or removed // and if the two were compared, it is unmodified if all properties were present with equal value if(propertyInfo.singleResourceStyle != null) resultingStyle[0] = propertyInfo.singleResourceStyle; else resultingStyle[0] = propertyInfo.modifiedCount == 0 ? STYLE_UnModified : STYLE_Modified; // The title can never differ as that means different resources - it is either a single catalog // (the non null catalog), or the newCatalog in case both are passed. // Add a labelRow for the 'type[id]' StringBuilder builder = new StringBuilder(); if(resultingStyle[0].equals(STYLE_Added)) { builder.append(GT); builder.append(" "); } else if(resultingStyle[0].equals(STYLE_Removed)) { builder.append(LT); builder.append(" "); } builder.append(singleResource.getType()); builder.append("["); builder.append(singleResource.getTitle()); builder.append("]"); boolean hasParameters = propertyInfo.width > 0; boolean hasFooter = singleResource.getFile() != null; // only show new Catalog file even if different List<LabelRow> labelRows = Lists.newArrayList(); if(hasParameters || hasFooter) labelRows.add(getStyles().labelRow( "RowSeparator", getStyles().labelCell("SpacingCell", "", Span.colSpan(1)))); labelRows.add(getStyles().labelRow(STYLE_ResourceTitleRow, // getStyles().labelCell(STYLE_ResourceTitleCell, builder.toString(), Span.colSpan(1)))); width = Math.max(width, builder.length()); // Rendering of separator line fails in graphviz 2.28 with an error // labelRows.add(getStyles().rowSeparator()); if(hasParameters || hasFooter) { labelRows.add(getStyles().labelRow( "RowSeparator", getStyles().labelCell("SpacingCell", "", Span.colSpan(1)))); labelRows.add(getStyles().labelRow("RowSeparator", getStyles().labelCell("HRCell", "", Span.colSpan(1)))); labelRows.add(getStyles().labelRow( "RowSeparator", getStyles().labelCell("SpacingCell", "", Span.colSpan(1)))); } // // OLD STYLE // labelRows.addAll(innerLabelRows); // NEW STYLE if(innerLabelRows.size() > 0) { LabelCell tableCell = getStyles().labelCell( "ResourceTableCell",// getStyles().labelTable(STYLE_ResourceTable, innerLabelRows.toArray(new LabelRow[innerLabelRows.size()]))); labelRows.add(getStyles().labelRow("ResourceTableRow", tableCell)); } // FOOTER // A footer with filename[line] // (is not always present) if(hasFooter) { builder = new StringBuilder(); // shorten the text by making it relative to root if possible if(singleRoot != null) builder.append(new Path(singleResource.getFile()).makeRelativeTo(singleRoot).toString()); else builder.append(singleResource.getFile()); if(singleResource.getLine() != null) { builder.append("["); builder.append(singleResource.getLine()); builder.append("]"); } String tooltip = builder.toString(); if(builder.length() > width) { builder.delete(0, builder.length() - width); builder.insert(0, "[...]"); } if(hasParameters) labelRows.add(getStyles().labelRow( "RowSeparator", getStyles().labelCell("SpacingCell", "", Span.colSpan(1)))); int line = -1; try { line = Integer.valueOf(singleResource.getLine()); } catch(NumberFormatException e) { line = -1; } labelRows.add(getStyles().labelRow( STYLE_ResourceFileInfoRow, // getStyles().labelCell(STYLE_ResourceFileInfoCell, builder.toString(), Span.colSpan(1)).withStyles( getStyles().tooltip(tooltip), // getStyles().href( getHrefProducer().hrefToManifest(new Path(singleResource.getFile()), singleRoot, line)) // )) // ); } else if(hasParameters) { // add a bit of padding at the bottom if there is no footer labelRows.add(getStyles().labelRow( "RowSeparator", getStyles().labelCell("SpacingCell", "", Span.colSpan(1)))); } return StyleSet.withStyle(// getStyles().labelFormat(// getStyles().labelTable(STYLE_ResourceTable, // labelRows.toArray(new LabelRow[labelRows.size()])))); } /** * Produces a graph in DOT format for the given catalog on the given output stream. * * @param cancel * enables cancellation of long running job * @param catalog * the catalog for which a graph is produced * @param out * where output in DOT format should be written * @param moduleData * Name -> 0* MetadataInfo representing one version of a module with given name */ public void produceGraph(ICancel cancel, String title, Catalog oldCatalog, IPath oldRoot, Catalog newCatalog, IPath newRoot, OutputStream out) { if(cancel == null || oldCatalog == null || newCatalog == null || out == null) throw new IllegalArgumentException("one or more parameters are null"); RootGraph g = produceRootGraph(cancel, title, oldCatalog, oldRoot, newCatalog, newRoot); getInstanceRules().addAll(getTheme().getInstanceRules()); getDotRenderer().write(cancel, out, g, getTheme().getDefaultRules(), getInstanceRules()); } /** * Produces the graph data structure (RootGraph, Vertexes, Edges). * */ private RootGraph produceRootGraph(ICancel cancel, String title, Catalog oldCatalog, IPath oldRoot, Catalog newCatalog, IPath newRoot) { RootGraph g = new RootGraph(title, "RootGraph", "root"); // Iterate the catalog // What to do with classes and tags? Are they of any value? // catalog.getClasses(); // list of classnames // catalog.getTags(); // don't know if these have any value... Map<String, Vertex> oldVertexMap = Maps.newHashMap(); Map<String, Vertex> newVertexMap = Maps.newHashMap(); Map<CatalogResource, Vertex> oldResourceVertexMap = Maps.newHashMap(); Map<CatalogResource, Vertex> newResourceVertexMap = Maps.newHashMap(); Map<Vertex, CatalogResource> catalogMap = Maps.newHashMap(); // create all vertexes for old createVertexesFor(oldCatalog.getResources(), oldVertexMap, oldResourceVertexMap, catalogMap); // create all vertexes for new createVertexesFor(newCatalog.getResources(), newVertexMap, newResourceVertexMap, catalogMap); // compute Venn set Set<String> oldKeys = oldVertexMap.keySet(); Set<String> newKeys = newVertexMap.keySet(); SetView<String> inBoth = Sets.intersection(oldKeys, newKeys); SetView<String> removedInNew = Sets.difference(oldKeys, newKeys); SetView<String> addedInNew = Sets.difference(newKeys, oldKeys); Map<String, Vertex> resultingVertexMap = Maps.newHashMap(); for(String s : removedInNew) { Vertex v = oldVertexMap.get(s); v.addStyleClass(STYLE_Removed); v.setStyles(labelStyleForResource(catalogMap.get(v), oldRoot, null, null, new String[1])); resultingVertexMap.put(s, v); g.addVertex(v); } for(String s : addedInNew) { Vertex v = newVertexMap.get(s); v.addStyleClass(STYLE_Added); v.setStyles(labelStyleForResource(null, null, catalogMap.get(v), newRoot, new String[1])); resultingVertexMap.put(s, v); g.addVertex(v); } for(String s : inBoth) { Vertex vOld = oldVertexMap.get(s); Vertex vNew = newVertexMap.get(s); Vertex v = new Vertex("", STYLE_Resource); String computedStyle[] = new String[1]; v.setStyles(labelStyleForResource( catalogMap.get(vOld), oldRoot, catalogMap.get(vNew), newRoot, computedStyle)); v.addStyleClass(computedStyle[0]); resultingVertexMap.put(s, v); g.addVertex(v); } // Process Edges Map<String, Edge> oldEdgeMap = Maps.newHashMap(); Map<String, Edge> newEdgeMap = Maps.newHashMap(); Set<String> edges = Sets.newHashSet(); edgesForCatalogEdges(oldCatalog.getEdges(), resultingVertexMap, oldEdgeMap, edges); edgesForResources(g, oldCatalog.getResources(), oldResourceVertexMap, resultingVertexMap, oldEdgeMap, edges); edgesForCatalogEdges(newCatalog.getEdges(), resultingVertexMap, newEdgeMap, edges); edgesForResources(g, newCatalog.getResources(), newResourceVertexMap, resultingVertexMap, newEdgeMap, edges); generateEdgeDelta(g, edges, oldEdgeMap, newEdgeMap); return g; } private String stringify(Iterable<String> iterable) { StringBuilder builder = new StringBuilder(); for(String v : iterable) { builder.append(v); builder.append(" "); } builder.deleteCharAt(builder.length() - 1); // trim size return builder.toString(); } }