/*
* 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.InputStream;
import java.io.PrintStream;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
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.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.w3c.dom.NamedNodeMap;
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(XMLSeries.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 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;
}
/***
* @param buildNumber
* the build number
* @returns a List of PlotPoints where the label is the element name and the
* value is the node content.
* @throws RunTimeException
* (NullPointerException)if a Node text content is not numeric
***/
private List<PlotPoint> mapNodeNameAsLabelTextContentAsValueStrategy(
NodeList nodeList, int buildNumber) {
List<PlotPoint> retval = new ArrayList<PlotPoint>();
for (int i = 0; i < nodeList.getLength(); i++) {
this.addNodeToList(retval, nodeList.item(i), buildNumber);
}
return retval;
}
/***
* This is a fallback strategy for nodesets that include non numeric content
* enabling users to create lists by selecting them such that names and
* values share a common parent. If a node has attributes and is empty that
* node will be re-enqueued as a parent to its attributes.
*
* @param buildNumber
* the build number
*
* @returns a list of PlotPoints where the label is the last non numeric
* text content and the value is the last numeric text content for
* each set of nodes under a given parent.
***/
private List<PlotPoint> coalesceTextnodesAsLabelsStrategy(
NodeList nodeList, int buildNumber) {
Map<Node, List<Node>> parentNodeMap = new HashMap<Node, List<Node>>();
for (int i = 0; i < nodeList.getLength(); i++) {
Node node = nodeList.item(i);
if (!parentNodeMap.containsKey(node.getParentNode())) {
parentNodeMap.put(node.getParentNode(), new ArrayList<Node>());
}
parentNodeMap.get(node.getParentNode()).add(node);
}
List<PlotPoint> retval = new ArrayList<PlotPoint>();
Queue<Node> parents = new ArrayDeque<Node>(parentNodeMap.keySet());
while (!parents.isEmpty()) {
Node parent = parents.poll();
Double value = null;
String label = null;
for (Node child : parentNodeMap.get(parent)) {
if (null == child.getTextContent()
|| child.getTextContent().trim().isEmpty()) {
NamedNodeMap attrmap = child.getAttributes();
List<Node> attrs = new ArrayList<Node>();
for (int i = 0; i < attrmap.getLength(); i++) {
attrs.add(attrmap.item(i));
}
parentNodeMap.put(child, attrs);
parents.add(child);
} else if (new Scanner(child.getTextContent().trim())
.hasNextDouble()) {
value = new Scanner(child.getTextContent().trim())
.nextDouble();
} else {
label = child.getTextContent().trim();
}
}
if ((label != null) && (value != null)) {
addValueToList(retval, new String(label),
String.valueOf(value), buildNumber);
}
}
return retval;
}
private void addValueToListFromAttributes(List<PlotPoint> retval, Node child) {
}
/**
* Load the series from a properties file.
*/
@Override
public List<PlotPoint> loadSeries(FilePath workspaceRootDir,
int buildNumber, 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.log(Level.SEVERE,
"Exception trying to retrieve series files", e);
return null;
}
if (ArrayUtils.isEmpty(seriesFiles)) {
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.log(Level.SEVERE,
"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 node = nl.item(i);
if (!new Scanner(node.getTextContent().trim())
.hasNextDouble()) {
return coalesceTextnodesAsLabelsStrategy(nl,
buildNumber);
}
}
return mapNodeNameAsLabelTextContentAsValueStrategy(nl,
buildNumber);
} else if (nodeType.equals(XPathConstants.NODE)) {
addNodeToList(ret, (Node) xmlObject, buildNumber);
} else {
// otherwise we have a single type and can do a toString on it.
if (xmlObject instanceof NodeList) {
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, label, xmlObject, buildNumber);
}
}
} else {
addValueToList(ret, label, xmlObject, buildNumber);
}
}
return ret;
} catch (XPathExpressionException e) {
LOGGER.log(Level.SEVERE, "XPathExpressionException for XPath '"
+ getXpath() + "'", e);
} finally {
IOUtils.closeQuietly(in);
}
return null;
}
private void addNodeToList(List<PlotPoint> ret, Node n, int buildNumber) {
NamedNodeMap nodeMap = n.getAttributes();
if ((null != nodeMap) && (null != nodeMap.getNamedItem("name"))) {
addValueToList(ret, nodeMap.getNamedItem("name").getTextContent()
.trim(), n, buildNumber);
} else {
addValueToList(ret, n.getLocalName().trim(), n, buildNumber);
}
}
/**
* 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
|| nodeType == XPathConstants.NODESET) {
if (obj instanceof String) {
ret = ((String) obj).trim();
} else {
if (null == obj) {
return null;
}
Node node = (Node) obj;
NamedNodeMap nodeMap = node.getAttributes();
if ((null != nodeMap) && (null != nodeMap.getNamedItem("time"))) {
ret = nodeMap.getNamedItem("time").getTextContent();
}
if (null == ret) {
ret = node.getTextContent().trim();
}
}
}
if (nodeType == XPathConstants.STRING)
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.
Scanner scanner = new Scanner(ret);
if (scanner.hasNextDouble()) {
return String.valueOf(scanner.nextDouble());
}
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
* @param buildNumber
*/
private void addValueToList(List<PlotPoint> list, String label,
Object nodeValue, int buildNumber) {
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(url, label, 0, buildNumber),
label));
} else {
if (LOGGER.isLoggable(defaultLogLevel))
LOGGER.log(defaultLogLevel, "Unable to add node: " + label
+ " value: " + nodeValue);
}
}
}