/* * 2012-3 Red Hat Inc. and/or its affiliates and other contributors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.overlord.rtgov.service.dependency.svg; import java.text.MessageFormat; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.namespace.QName; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.overlord.rtgov.analytics.service.InvocationDefinition; import org.overlord.rtgov.analytics.service.InvocationMetric; import org.overlord.rtgov.analytics.situation.Situation; import org.overlord.rtgov.service.dependency.InvocationLink; import org.overlord.rtgov.service.dependency.OperationNode; import org.overlord.rtgov.service.dependency.ServiceDependencyBuilder; import org.overlord.rtgov.service.dependency.ServiceGraph; import org.overlord.rtgov.service.dependency.ServiceNode; import org.overlord.rtgov.service.dependency.UsageLink; import org.overlord.rtgov.service.dependency.layout.ServiceGraphLayout; import org.overlord.rtgov.service.dependency.presentation.Severity; import org.overlord.rtgov.service.dependency.presentation.SeverityAnalyzer; /** * This class generates a SVG representation of a * service graph. * */ public class SVGServiceGraphGenerator { private static final int MIN_HEIGHT = 400; private static final int HORIZONTAL_PADDING = 100; private static final Logger LOG=Logger.getLogger(SVGServiceGraphGenerator.class.getName()); private SeverityAnalyzer _severityAnalyzer=null; private static String[] _colorCodes={ "#00FF00", // Normal "#FF9900", // Minor Warning "#FF6A45", // Requires Investigation "#FF5930", // Error "#FF3300", // Serious "#FF0000" // Critical }; /** * The default constructor. */ public SVGServiceGraphGenerator() { } /** * This method returns the color selector. * * @return The color selector */ public SeverityAnalyzer getSeverityAnalyzer() { return (_severityAnalyzer); } /** * This method sets the severity analyzer. * * @param analyzer The severity analyzer */ public void setSeverityAnalyzer(SeverityAnalyzer analyzer) { _severityAnalyzer = analyzer; } /** * This method generates the SVG representation of the supplied * service graph to the output stream. * * @param sg The service graph * @param maxWidth The maximum width, or 0 if not relevant * @param os The output stream * @throws Exception Failed to generate SVG */ public void generate(ServiceGraph sg, int maxWidth, java.io.OutputStream os) throws Exception { double ratio=1.0; if (maxWidth > 0) { int width=(Integer)sg.getProperties().get(ServiceGraphLayout.WIDTH); ratio = (double)maxWidth / (double)width; } org.w3c.dom.Document doc=loadTemplate(ratio < 0.8 ? "summary" : "main"); if (doc != null) { org.w3c.dom.Element container=doc.getDocumentElement(); // Set the width and height container.setAttribute("width", "100%"); int height=(Integer)sg.getProperties().get(ServiceGraphLayout.HEIGHT); height += HORIZONTAL_PADDING; if (LOG.isLoggable(Level.FINEST)) { LOG.finest("SVG height = "+height); } if (height < MIN_HEIGHT) { height = MIN_HEIGHT; } container.setAttribute("height", ""+height); org.w3c.dom.Node insertPoint=null; org.w3c.dom.NodeList nl=container.getElementsByTagName("insert"); if (nl.getLength() == 1) { insertPoint = nl.item(0); } // Add description if (sg.getDescription() != null && ratio >= 1.0) { org.w3c.dom.Element text= container.getOwnerDocument().createElement("text"); text.setAttribute("x", "10"); text.setAttribute("y", "20"); text.setAttribute("font-size", "14"); text.setAttribute("font-family", "Verdana"); text.setAttribute("fill", "black"); container.insertBefore(text, insertPoint); org.w3c.dom.Text textValue= container.getOwnerDocument().createTextNode(sg.getDescription()); text.appendChild(textValue); } if (sg.getServiceNodes().size() > 0) { // Generate nodes and links for (ServiceNode sn : sg.getServiceNodes()) { generateService(sn, ratio, container, insertPoint); } for (UsageLink ul : sg.getUsageLinks()) { generateUsageLink(ul, ratio, container, insertPoint); } for (InvocationLink il : sg.getInvocationLinks()) { generateInvocationLink(il, ratio, container, insertPoint); } } else { org.w3c.dom.Element text= container.getOwnerDocument().createElement("text"); text.setAttribute("x", "50"); text.setAttribute("y", "100"); text.setAttribute("font-size", "30"); text.setAttribute("font-family", "Verdana"); text.setAttribute("fill", "#D9D9C2"); container.insertBefore(text, insertPoint); org.w3c.dom.Text textValue= container.getOwnerDocument().createTextNode("No Service Activity"); text.appendChild(textValue); } // Remove insertion point if (insertPoint != null) { container.removeChild(insertPoint); } saveDocument(doc, os); } } /** * This method generates the usage link. * * @param ul The usage link * @param ratio The ratio * @param container The container * @param insertPoint The insertion point */ protected void generateUsageLink(UsageLink ul, double ratio, org.w3c.dom.Element container, org.w3c.dom.Node insertPoint) { // Check that layout information is available if (!ul.getSource().getProperties().containsKey(ServiceGraphLayout.WIDTH)) { if (LOG.isLoggable(Level.FINER)) { LOG.fine("Source node on usage link with definition '"+ul.getSource().getService()+"' does not have layout information"); } return; } if (!ul.getTarget().getProperties().containsKey(ServiceGraphLayout.WIDTH)) { if (LOG.isLoggable(Level.FINER)) { LOG.fine("Target node on usage link with definition '"+ul.getTarget().getService()+"' does not have layout information"); } return; } int x1=(int)(((Integer)ul.getSource().getProperties().get(ServiceGraphLayout.X_POSITION) +(Integer)ul.getSource().getProperties().get(ServiceGraphLayout.WIDTH)) * ratio); int y1=(int)(((Integer)ul.getSource().getProperties().get(ServiceGraphLayout.Y_POSITION)) * ratio); int x2=(int)(((Integer)ul.getTarget().getProperties().get(ServiceGraphLayout.X_POSITION)) * ratio); int y2=(int)(((Integer)ul.getTarget().getProperties().get(ServiceGraphLayout.Y_POSITION)) * ratio); int x3=(int)(((Integer)ul.getTarget().getProperties().get(ServiceGraphLayout.X_POSITION)) * ratio); int y3=(int)(((Integer)ul.getTarget().getProperties().get(ServiceGraphLayout.Y_POSITION) +(Integer)ul.getTarget().getProperties().get(ServiceGraphLayout.HEIGHT)) * ratio); int x4=(int)(((Integer)ul.getSource().getProperties().get(ServiceGraphLayout.X_POSITION) +(Integer)ul.getSource().getProperties().get(ServiceGraphLayout.WIDTH)) * ratio); int y4=(int)(((Integer)ul.getSource().getProperties().get(ServiceGraphLayout.Y_POSITION) +(Integer)ul.getSource().getProperties().get(ServiceGraphLayout.HEIGHT)) * ratio); org.w3c.dom.Element polygon= container.getOwnerDocument().createElement("polygon"); polygon.setAttribute("points", x1+","+y1+" "+x2+","+y2+" " +x3+","+y3+" "+x4+","+y4); Severity severity=getInvocationSeverity(ul.getInvocations()); String color=getColor(severity); polygon.setAttribute("style", "fill:"+color+";fill-opacity:0.2"); container.insertBefore(polygon, insertPoint); if (ratio >= 1.0) { // Generate tooltip generateMetrics(polygon, getDescription(ul), ServiceDependencyBuilder.getMergedMetrics(ul.getInvocations())); } } /** * This method returns the description to be used for the * usage link. * * @param ul The usage link * @return The description */ protected String getDescription(UsageLink ul) { return (ul.getSource().getService().getServiceType() +" -> "+ul.getTarget().getService().getServiceType()); } /** * This method returns the colour code associated with the severity. * * @param severity The severity * @return The colour code */ protected String getColor(Severity severity) { if (severity == null) { return (_colorCodes[0]); } return (_colorCodes[severity.ordinal()]); } /** * This method generates the invocation link. * * @param il The invocation link * @param ratio The ratio * @param container The container * @param insertPoint The insertion point */ protected void generateInvocationLink(InvocationLink il, double ratio, org.w3c.dom.Element container, org.w3c.dom.Node insertPoint) { // Check that layout information is available if (!il.getSource().getProperties().containsKey(ServiceGraphLayout.WIDTH)) { if (LOG.isLoggable(Level.FINER)) { LOG.fine("Source node on invocation link with definition '"+il.getSource().getService()+"' does not have layout information"); } return; } if (!il.getTarget().getProperties().containsKey(ServiceGraphLayout.WIDTH)) { if (LOG.isLoggable(Level.FINER)) { LOG.fine("Target node on invocation link with definition '"+il.getTarget().getService()+"' does not have layout information"); } return; } int x1=(int)(((Integer)il.getSource().getProperties().get(ServiceGraphLayout.X_POSITION) +(Integer)il.getSource().getProperties().get(ServiceGraphLayout.WIDTH)) * ratio); int y1=(int)(((Integer)il.getSource().getProperties().get(ServiceGraphLayout.Y_POSITION) +(Integer)il.getSource().getProperties().get(ServiceGraphLayout.HEIGHT)/2) * ratio); int x2=(int)(((Integer)il.getTarget().getProperties().get(ServiceGraphLayout.X_POSITION)) * ratio); int y2=(int)(((Integer)il.getTarget().getProperties().get(ServiceGraphLayout.Y_POSITION) +(Integer)il.getTarget().getProperties().get(ServiceGraphLayout.HEIGHT)/2) * ratio); org.w3c.dom.Element line= container.getOwnerDocument().createElement("line"); line.setAttribute("x1", ""+x1); line.setAttribute("y1", ""+y1); line.setAttribute("x2", ""+x2); line.setAttribute("y2", ""+y2); Severity severity=getInvocationSeverity(il.getInvocations()); String color=getColor(severity); line.setAttribute("style", "stroke:"+color+";stroke-width:3"); container.insertBefore(line, insertPoint); if (ratio >= 1.0) { // Generate tooltip generateMetrics(line, getDescription(il), ServiceDependencyBuilder.getMergedMetrics(il.getInvocations())); } } /** * This method returns the description to be used for the * invocation link. * * @param il The invocation link * @return The description */ protected String getDescription(InvocationLink il) { return (il.getTarget().getService().getServiceType() +" -> "+il.getTarget().getOperation().getName()); } /** * This method generates the service node. * * @param sn The service node * @param ratio The ratio, 1 if normal size * @param container The container * @param insertPoint The insertion point */ protected void generateService(ServiceNode sn, double ratio, org.w3c.dom.Element container, org.w3c.dom.Node insertPoint) { // Check that layout information is available if (!sn.getProperties().containsKey(ServiceGraphLayout.WIDTH)) { if (LOG.isLoggable(Level.FINER)) { LOG.fine("Service node with definition '"+sn.getService()+"' does not have layout information"); } return; } org.w3c.dom.Element rect= container.getOwnerDocument().createElement("rect"); int width=(int)((Integer)sn.getProperties().get(ServiceGraphLayout.WIDTH) * ratio); rect.setAttribute("width", ""+width); int height=(int)((Integer)sn.getProperties().get(ServiceGraphLayout.HEIGHT) * ratio); rect.setAttribute("height", ""+height); int x=(int)((Integer)sn.getProperties().get(ServiceGraphLayout.X_POSITION) * ratio); rect.setAttribute("x", ""+x); int y=(int)((Integer)sn.getProperties().get(ServiceGraphLayout.Y_POSITION) * ratio); rect.setAttribute("y", ""+y); rect.setAttribute("fill", "#B8DBFF"); // Derive the color Severity severity=null; if (getSeverityAnalyzer() != null) { severity = getSeverityAnalyzer().getSeverity(sn, sn.getService().getMetrics(), sn.getService().getHistory()); } String color=getColor(severity); rect.setAttribute("stroke", color); rect.setAttribute("stroke-width", "2"); rect.setAttribute("filter", "url(#f1)"); container.insertBefore(rect, insertPoint); x= (Integer)sn.getProperties().get(ServiceGraphLayout.X_POSITION); x += 5; x *= ratio; y=(Integer)sn.getProperties().get(ServiceGraphLayout.Y_POSITION); y += 10; y *= ratio; if (isGenerateToolTips(ratio)) { // Generate tooltip generateMetrics(rect, sn.getService().getServiceType(), sn.getService().getMetrics()); } // Generate text org.w3c.dom.Element text= container.getOwnerDocument().createElement("text"); text.setAttribute("x", ""+x); text.setAttribute("y", ""+y); text.setAttribute("font-family", "Verdana"); text.setAttribute("font-size", "10"); text.setAttribute("fill", "#00008F"); String localname=getLocalName(sn.getService().getServiceType()); org.w3c.dom.Text value= container.getOwnerDocument().createTextNode( localname); text.appendChild(value); container.insertBefore(text, insertPoint); if (sn.getSituations().size() > 0) { generateSituations(container, insertPoint, x+(int)(width * 0.9), y, ratio, sn.getSituations()); } // Generate operations for (OperationNode opn : sn.getOperations()) { generateOperation(opn, ratio, container, insertPoint); } } /** * This method returns the local name associated with the supplied * fully qualified name. * * @param qname The fully qualified name * @return The local name */ protected String getLocalName(String qname) { String ret=qname; if (qname.length() > 0) { if (qname.charAt(0) == '{') { ret = QName.valueOf(qname).getLocalPart(); } else { int pos=qname.lastIndexOf('.'); if (pos != -1) { ret = qname.substring(pos+1); } } } return (ret); } /** * This method determines whether to generate the tool tips. * * @param ratio The ratio * @return Whether to generate tool tips */ protected boolean isGenerateToolTips(double ratio) { return (ratio >= 1.0); } /** * This method determines whether to generate the text. * * @param ratio The ratio * @return Whether to generate text */ protected boolean isGenerateText(double ratio) { return (ratio >= 1.0); } /** * This method generates the situations associated with the supplied * position and list. * * @param container The container * @param x The x position * @param y The y position * @param ratio The ratio * @param situations The list of situations */ protected void generateSituations(org.w3c.dom.Element container, org.w3c.dom.Node insertPoint, int x, int y, double ratio, java.util.List<Situation> situations) { int radius=(int)((double)6 * ratio); org.w3c.dom.Element circle= container.getOwnerDocument().createElement("circle"); circle.setAttribute("cx", ""+x); circle.setAttribute("cy", ""+y); circle.setAttribute("r", ""+radius); //circle.setAttribute("stroke-width", "1"); //circle.setAttribute("stroke", "black"); container.insertBefore(circle, insertPoint); Situation.Severity severity=Situation.getHighestSeverity(situations); if (severity != null) { String color=getSituationSeverityColor(severity); circle.setAttribute("fill", color); } if (isGenerateToolTips(ratio)) { // Generate the situation elements org.w3c.dom.Element title= container.getOwnerDocument().createElement("desc"); circle.appendChild(title); org.w3c.dom.Text titleText= container.getOwnerDocument().createTextNode("Situations"); title.appendChild(titleText); java.util.List<Situation> critical=Situation.getSituationsForSeverity( Situation.Severity.Critical, situations); if (critical.size() > 0) { org.w3c.dom.Element desc= container.getOwnerDocument().createElement( Situation.Severity.Critical.name()); circle.appendChild(desc); org.w3c.dom.Text descText= container.getOwnerDocument().createTextNode( getSituationText(critical.get(0))); desc.appendChild(descText); } java.util.List<Situation> high=Situation.getSituationsForSeverity( Situation.Severity.High, situations); if (high.size() > 0) { org.w3c.dom.Element desc= container.getOwnerDocument().createElement( Situation.Severity.High.name()); circle.appendChild(desc); org.w3c.dom.Text descText= container.getOwnerDocument().createTextNode( getSituationText(high.get(0))); desc.appendChild(descText); } java.util.List<Situation> medium=Situation.getSituationsForSeverity( Situation.Severity.Medium, situations); if (medium.size() > 0) { org.w3c.dom.Element desc= container.getOwnerDocument().createElement( Situation.Severity.Medium.name()); circle.appendChild(desc); org.w3c.dom.Text descText= container.getOwnerDocument().createTextNode( getSituationText(medium.get(0))); desc.appendChild(descText); } java.util.List<Situation> low=Situation.getSituationsForSeverity( Situation.Severity.Low, situations); if (low.size() > 0) { org.w3c.dom.Element desc= container.getOwnerDocument().createElement( Situation.Severity.Low.name()); circle.appendChild(desc); org.w3c.dom.Text descText= container.getOwnerDocument().createTextNode( getSituationText(low.get(0))); desc.appendChild(descText); } } } /** * This method returns the text to display for the supplied * situation. * * @param s The situation * @return The text */ protected String getSituationText(Situation s) { return (s.getType()+": "+s.getDescription()+" [" +new java.util.Date(s.getTimestamp())+"]"); } /** * This method returns the color associated with the supplied * situation severity. * * @param severity The situation severity * @return The color */ protected String getSituationSeverityColor(Situation.Severity severity) { String ret=null; if (severity == Situation.Severity.Critical) { ret = "red"; } else if (severity == Situation.Severity.High) { ret = "orange"; } else if (severity == Situation.Severity.Medium) { ret = "yellow"; } else { ret = "green"; } return (ret); } /** * This method returns the invocation metrics associated with the * supplied invocation definitions. * * @param invocations The invocation definitions * @return The severity */ protected Severity getInvocationSeverity(java.util.List<InvocationDefinition> invocations) { if (getSeverityAnalyzer() == null) { return (Severity.Normal); } Severity[] severities=new Severity[invocations.size()]; for (int i=0; i < invocations.size(); i++) { InvocationDefinition id=invocations.get(i); severities[i] = getSeverityAnalyzer().getSeverity(id, id.getMetrics(), id.getHistory()); } return (getAverageSeverity(severities)); } /** * Determine the average severity based on the supplied array. * * @param severities The severities * @return The average */ protected static Severity getAverageSeverity(Severity[] severities) { Severity ret=Severity.Normal; int sum=0; for (Severity s : severities) { sum = s.ordinal(); } if (sum > 0) { float result=sum / severities.length; ret = Severity.values()[Math.round(result)]; } return (ret); } /** * This method generates the tooltip information to show * metrics. * * @param container The container * @param description The description * @param metrics The metrics */ protected void generateMetrics(org.w3c.dom.Element container, String description, InvocationMetric metrics) { org.w3c.dom.Element desc= container.getOwnerDocument().createElement("desc"); container.appendChild(desc); org.w3c.dom.Text descText= container.getOwnerDocument().createTextNode(description); desc.appendChild(descText); String countValue="Count "+metrics.getCount(); String avgValue="Avg "+metrics.getAverage(); String minValue="Min "+metrics.getMin(); String maxValue="Max "+metrics.getMax(); if (metrics.getFaults() > 0) { countValue += " [faults "+metrics.getFaults()+"]"; } if (metrics.getAverageChange() != 0 || metrics.getMaxChange() != 0 || metrics.getMinChange() != 0 || metrics.getCountChange() != 0) { countValue += " ("+metrics.getCountChange()+"%)"; avgValue += " ("+metrics.getAverageChange()+"%)"; minValue += " ("+metrics.getMinChange()+"%)"; maxValue += " ("+metrics.getMaxChange()+"%)"; } org.w3c.dom.Element count= container.getOwnerDocument().createElement("count"); container.appendChild(count); org.w3c.dom.Text countText= container.getOwnerDocument().createTextNode(countValue); count.appendChild(countText); org.w3c.dom.Element avg= container.getOwnerDocument().createElement("avg"); container.appendChild(avg); org.w3c.dom.Text avgText= container.getOwnerDocument().createTextNode(avgValue); avg.appendChild(avgText); org.w3c.dom.Element max= container.getOwnerDocument().createElement("max"); container.appendChild(max); org.w3c.dom.Text maxText= container.getOwnerDocument().createTextNode(maxValue); max.appendChild(maxText); org.w3c.dom.Element min= container.getOwnerDocument().createElement("min"); container.appendChild(min); org.w3c.dom.Text minText= container.getOwnerDocument().createTextNode(minValue); min.appendChild(minText); } /** * This method generates the operation node. * * @param opn The operation node * @param ratio The ratio * @param container The container element * @param insertPoint The insertion point */ protected void generateOperation(OperationNode opn, double ratio, org.w3c.dom.Element container, org.w3c.dom.Node insertPoint) { org.w3c.dom.Element rect= container.getOwnerDocument().createElement("rect"); int width=(int)((Integer)opn.getProperties().get(ServiceGraphLayout.WIDTH) * ratio); rect.setAttribute("width", ""+width); int height=(int)((Integer)opn.getProperties().get(ServiceGraphLayout.HEIGHT) * ratio); rect.setAttribute("height", ""+height); int x=(int)((Integer)opn.getProperties().get(ServiceGraphLayout.X_POSITION) * ratio); rect.setAttribute("x", ""+x); int y=(int)((Integer)opn.getProperties().get(ServiceGraphLayout.Y_POSITION) * ratio); rect.setAttribute("y", ""+y); rect.setAttribute("fill", "#85D6FF"); // Derive the colour Severity severity=null; if (getSeverityAnalyzer() != null) { severity = getSeverityAnalyzer().getSeverity(opn, opn.getOperation().getMetrics(), opn.getOperation().getHistory()); } String color=getColor(severity); rect.setAttribute("stroke", color); rect.setAttribute("stroke-width", "1"); container.insertBefore(rect, insertPoint); x = (Integer)opn.getProperties().get(ServiceGraphLayout.X_POSITION); x += 5; x *= ratio; y = (Integer)opn.getProperties().get(ServiceGraphLayout.Y_POSITION); y += 14; y *= ratio; if (isGenerateText(ratio)) { org.w3c.dom.Element text= container.getOwnerDocument().createElement("text"); text.setAttribute("x", ""+x); text.setAttribute("y", ""+y); text.setAttribute("font-family", "Verdana"); text.setAttribute("font-size", "12"); text.setAttribute("fill", "#00008F"); org.w3c.dom.Text value= container.getOwnerDocument().createTextNode( opn.getOperation().getName()); text.appendChild(value); container.insertBefore(text, insertPoint); } if (isGenerateToolTips(ratio)) { // Generate tooltip generateMetrics(rect, opn.getOperation().getName(), opn.getOperation().getMetrics()); } if (opn.getSituations().size() > 0) { generateSituations(container, insertPoint, x+(int)(width * 0.9), y-4, ratio, opn.getSituations()); } } /** * This method saves the supplied document to the output stream. * * @param doc The SVG document * @param os The output stream * @throws Exception Failed to save SVG document */ protected void saveDocument(org.w3c.dom.Document doc, java.io.OutputStream os) throws Exception { // Write out SVG document javax.xml.transform.Transformer transformer= javax.xml.transform.TransformerFactory.newInstance().newTransformer(); transformer.transform(new javax.xml.transform.dom.DOMSource(doc), new javax.xml.transform.stream.StreamResult(os)); } /** * This method returns the named template file. * * @param name The svg template name * @return The template, or null if failed to load */ protected org.w3c.dom.Document loadTemplate(String name) { org.w3c.dom.Document ret=null; try { String template="templates/" + name + ".svg"; java.io.InputStream is= Thread.currentThread().getContextClassLoader().getResourceAsStream("/"+template); if (is == null) { is = Thread.currentThread().getContextClassLoader().getResourceAsStream(template); } DocumentBuilder builder=DocumentBuilderFactory.newInstance().newDocumentBuilder(); ret = builder.parse(is); is.close(); } catch (Exception e) { LOG.log(Level.SEVERE, MessageFormat.format(java.util.PropertyResourceBundle.getBundle( "service-dependency-svg.Messages").getString("SERVICE-DEPENDENCY-SVG-1"), name), e); } return (ret); } }