/* * Copyright (c) 2008-2009 Yahoo! Inc. All rights reserved. * The copyrights to the contents of this file are licensed under the MIT License (http://www.opensource.org/licenses/mit-license.php) */ package hudson.plugins.plot; import hudson.FilePath; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.ServletException; import javax.xml.namespace.QName; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.kohsuke.stapler.DataBoundConstructor; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; /** * Represents a plot data series configuration from an XML file. * * @author Allen Reese * */ public class XMLSeries extends Series { private static transient final Logger LOGGER = Logger.getLogger(Series.class.getName()); // Debugging hack, so I don't have to change FINE/INFO... private static transient final Level defaultLogLevel = Level.INFO; private static transient final Pattern PAT_NAME = Pattern.compile("%name%"); private static transient final Pattern PAT_INDEX = Pattern.compile("%index%"); private static transient final Map<String,QName> qnameMap; /** * Fill out the qname map for easy reference. */ static { HashMap<String, QName> tempMap = new HashMap<String, QName>(); tempMap.put("BOOLEAN", XPathConstants.BOOLEAN); tempMap.put("NODE", XPathConstants.NODE); tempMap.put("NODESET", XPathConstants.NODESET); tempMap.put("NUMBER", XPathConstants.NUMBER); tempMap.put("STRING", XPathConstants.STRING); qnameMap = Collections.unmodifiableMap(tempMap); } /** * XPath to select for values */ private String xpathString; /** * Url to use as a base for mapping points. */ private String url; /** * String of the qname type to use */ private String nodeTypeString; /** * Actual nodeType */ private transient QName nodeType; /** * * @param file * @param label * @param req Stapler request * @param radioButtonId ID used to find the parameters specific to this instance. * @throws ServletException */ @DataBoundConstructor public XMLSeries(String file, String xpath, String nodeType, String url) { super(file, "", "xml"); this.xpathString = xpath; this.nodeTypeString = nodeType; this.nodeType = qnameMap.get(nodeType); this.url = url; } private Object readResolve() { // Set nodeType when deserialized nodeType = qnameMap.get(nodeTypeString); return this; } public String getXpath() { return xpathString; } public String getNodeType() { return nodeTypeString; } public String getUrl() { return url; } /** * Load the series from a properties file. */ @Override public PlotPoint[] loadSeries(FilePath workspaceRootDir, PrintStream logger) { InputStream in = null; InputSource inputSource = null; try { List<PlotPoint> ret = new ArrayList<PlotPoint>(); FilePath[] seriesFiles = null; try { seriesFiles = workspaceRootDir.list(getFile()); } catch (Exception e) { LOGGER.warning("Exception trying to retrieve series files: " + e); return null; } if (seriesFiles != null && seriesFiles.length < 1) { LOGGER.info("No plot data file found: " + getFile()); return null; } try { if (LOGGER.isLoggable(defaultLogLevel)) LOGGER.log(defaultLogLevel,"Loading plot series data from: " + getFile()); in = seriesFiles[0].read(); // load existing plot file inputSource = new InputSource(in); } catch (Exception e) { LOGGER.warning("Exception reading plot series data from: " + seriesFiles[0] + " " + e); return null; } if (LOGGER.isLoggable(defaultLogLevel)) LOGGER.log(defaultLogLevel,"NodeType " + nodeTypeString + " : " + nodeType); if (LOGGER.isLoggable(defaultLogLevel)) LOGGER.log(defaultLogLevel,"Loaded XML Plot file: " + getFile()); XPath xpath = XPathFactory.newInstance().newXPath(); Object xmlObject = xpath.evaluate(xpathString, inputSource, nodeType); /* * If we have a nodeset, we need multiples, otherwise we just need one value, and can do a toString() * to set it. */ if (nodeType.equals(XPathConstants.NODESET)) { NodeList nl=(NodeList)xmlObject; if (LOGGER.isLoggable(defaultLogLevel)) LOGGER.log(defaultLogLevel,"Number of nodes: " + nl.getLength()); for (int i = 0; i < nl.getLength(); i++) { Node n = nl.item(i); if (n != null && n.getLocalName() != null && n.getTextContent() != null) { addValueToList(ret, n.getLocalName().trim(), n.getTextContent().trim()); } } } else { // otherwise we have a single type and can do a toString on it. addValueToList(ret,label, xmlObject); } return ret.toArray(new PlotPoint[ret.size()]); } catch (XPathExpressionException e) { //ignore if (LOGGER.isLoggable(defaultLogLevel)) LOGGER.log(defaultLogLevel,"Exception: " + e); } finally { if (in != null) { try { in.close(); } catch (IOException ignore) { //ignore } } } return null; } /** * Return the url that should be used for this point. * @param label Name of the column * @param index Index of the column * @return url for the label. */ private String getUrl(String label,int index) { /* * Check the name first, and do replacement upon it. */ Matcher nameMatcher = PAT_NAME.matcher(label); if (nameMatcher.find()) { url = nameMatcher.replaceAll(label); } /* * Check the index, and do replacement on it. */ Matcher indexMatcher = PAT_INDEX.matcher(label); if (indexMatcher.find()) { url = indexMatcher.replaceAll(label); } return url; } /** * Convert a given object into a String. * @param obj Xpath Object * @return String representation of the node */ private String nodeToString(Object obj) { String ret = null; if (nodeType==XPathConstants.BOOLEAN) { return (((Boolean)obj))?"1":"0"; } if (nodeType==XPathConstants.NUMBER) return ((Double)obj).toString().trim(); if (nodeType==XPathConstants.NODE) ret = ((Node)obj).toString().trim(); if (nodeType==XPathConstants.STRING || nodeType==XPathConstants.NODESET) ret = ((String)obj).trim(); // for Node/String/NodeSet, try and parse it as a double. // we don't store a double, so just throw away the result. if (ret != null) { try { Double.parseDouble(ret); return ret; } catch (NumberFormatException ignore) { } } return null; } /** * Add a given value to the list of results. * This encapsulates some otherwise duplicate logic due to nodeset/!nodeset * @param list * @param label * @param nodeValue */ private void addValueToList(List<PlotPoint> list, String label, Object nodeValue) { String value = nodeToString(nodeValue); if (value != null) { if (LOGGER.isLoggable(defaultLogLevel)) LOGGER.log(defaultLogLevel, "Adding node: " + label + " value: " + value); list.add(new PlotPoint(value, getUrl(label,0), label)); } else { if (LOGGER.isLoggable(defaultLogLevel)) LOGGER.log(defaultLogLevel, "Unable to add node: " + label + " value: " + nodeValue); } } }