/*
* Rapid Beans Framework: XmlHelper.java
*
* Copyright (C) 2010 Martin Bluemel
*
* Creation Date: 09/12/2010
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by the Free Software Foundation;
* either version 3 of the License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
* You should have received a copies of the GNU Lesser General Public License and the
* GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
package org.rapidbeans.core.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.StringTokenizer;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.rapidbeans.core.exception.RapidBeansRuntimeException;
import org.rapidbeans.core.exception.UtilException;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* @author Martin Bluemel
*
* Utility class for easy XML parsing
*/
public class XmlHelper {
/**
* get the first XML sub node.
*
* @param node
* - parent node
* @param pattern
* - node pattern
*
* @return - sub node
*/
public static Node getFirstSubnode(final Node node, final String pattern) {
Node[] subnodes = getSubnodes(node, pattern);
if (subnodes.length == 0) {
throw new RapidBeansRuntimeException("XML Util Error: could not find subnode \"" + pattern
+ "\" of node \"" + node.getNodeName() + "\"");
}
return subnodes[0];
}
public static Node[] getSubnodes(final Node node, final String pattern) {
String firstPatternToken = null;
if (pattern != null) {
firstPatternToken = new StringTokenizer(pattern, "/").nextToken();
}
final NodeList subnodes = node.getChildNodes();
final int subnodesCount = subnodes.getLength();
final List<Node> foundNodes = new ArrayList<Node>();
for (int i = 0; i < subnodesCount; i++) {
if (firstPatternToken == null || subnodes.item(i).getNodeName().equals(firstPatternToken)) {
foundNodes.add(subnodes.item(i));
}
}
Node[] ret = new Node[0];
ret = (Node[]) foundNodes.toArray(ret);
return ret;
}
public static String getNodeValue(final Node node, final String pattern) {
return getNodeValue(node, pattern, null);
}
public static String getNodeValue(final Node startNode, final String pattern, final String defaultValue) {
String ret = defaultValue;
Node node = getNode(startNode, pattern);
if (node != null) {
final Node firstChild = node.getFirstChild();
if (firstChild != null) {
ret = firstChild.getNodeValue();
} else {
ret = node.getNodeValue();
}
}
return ret;
}
/**
* Helper function using Java XPath support.
*
* @param node
* an arbitrary XML element node to start the search with
* @param nodePathPattern
* An XPath expression
* - e. g.: //Server/Service/Connector/@port
* Service/Connector[2]/@port
*
* @return a list with found nodes (might be empty)
*/
public static NodeList getNodes(final Document doc, final String xPathExpression) {
XPath xPath = XPathFactory.newInstance().newXPath();
NodeList nodes;
try {
nodes = (NodeList) xPath.evaluate(xPathExpression, doc.getDocumentElement(), XPathConstants.NODESET);
} catch (XPathExpressionException e) {
throw new UtilException("Error while parsing XPath expression \"" + xPathExpression + "\"", e);
}
return nodes;
}
/**
* Self written search function with XPath like expression syntax.
* Not recommended if you have the document node.
* Then please prefer the getNode() and getNodes() functions above.
*
* @param node
* an arbitrary XML element node to start the search with
* @param nodePathPattern
* An XPath like expression with strong restrictions against XPath
* - e. g.: //Server/Service/Connector/@port
* Service/Connector[2]/@port
*
* @return found element or attribute node / subnode
*/
public static Node getNode(final Node startNode, final String xPathExpression) {
if (StringHelper.trim(xPathExpression).equals("")) {
return startNode;
}
Node foundNode = null;
if (startNode instanceof Document) {
final XPath xPath = XPathFactory.newInstance().newXPath();
try {
foundNode = (Node) xPath.evaluate(xPathExpression, ((Document) startNode).getDocumentElement(),
XPathConstants.NODE);
} catch (XPathExpressionException e) {
throw new UtilException("Error while parsing XPath expression \"" + xPathExpression + "\"", e);
}
} else {
final StringTokenizer st = new StringTokenizer(xPathExpression, "/");
final String firstPatternToken = st.nextToken();
if (firstPatternToken.startsWith("@")) {
final String attrName = firstPatternToken.substring(1);
foundNode = startNode.getAttributes().getNamedItem(attrName);
} else {
final String subnodePathPattern = getSubnodePathPattern(xPathExpression, firstPatternToken);
final String subnodeName = getSubnodePathToken(firstPatternToken);
final SubnodeFilter subnodeFilter = createSubnodeFilter(new XmlHelper(), firstPatternToken);
final Node[] subnodes = getSubnodes(startNode, subnodeName);
if (subnodes != null && subnodes.length > 0) {
foundNode = subnodeFilter.filter(subnodes, subnodePathPattern);
}
}
}
return foundNode;
}
/**
* extracts the name.
*
* @param pattern
* - xxx
* @param firstPatternToken
* - xxx
* @return node name without order e. g. xxx[2] -> xxx
*/
private static String getSubnodePathPattern(final String pattern, final String firstPatternToken) {
switch (StringHelper.split(pattern, "/").size()) {
case 0:
case 1:
return "";
default:
if (pattern.startsWith("//")) {
return pattern.substring(firstPatternToken.length() + 3);
} else if (pattern.startsWith("/")) {
return pattern.substring(firstPatternToken.length() + 2);
} else {
if (pattern.length() > firstPatternToken.length()) {
return pattern.substring(firstPatternToken.length() + 1);
} else {
return "";
}
}
}
}
/**
* extracts the parent.
*
* @param nodePattern
* nodepattern with parent
* @return the parent
*/
public static String getParentNodePattern(final String nodePattern) {
return StringHelper.splitBeforeLast(nodePattern, "/");
}
/**
* extracts the attribute name.
*
* @param nodePattern
* - node pattern with attribute
* @return attribute name
*/
public static String getAttributeName(final String nodePattern) {
StringTokenizer st = new StringTokenizer(nodePattern, "/");
String token = null;
while (st.hasMoreTokens()) {
token = st.nextToken();
}
if (!token.startsWith("@")) {
throw new RapidBeansRuntimeException("pattern \"" + nodePattern + "\" does not define an attribute");
}
return token.substring(1);
}
/**
* extracts the name.
*
* @param patternToken
* - XML path component
* @return node name without order e. g. xxx[2] -> xxx
*/
private static String getSubnodePathToken(final String patternToken) {
if (patternToken.indexOf('[') > -1) {
return patternToken.substring(0, patternToken.indexOf('['));
} else {
return patternToken;
}
}
/**
* search for the top level document.
*
* @param xmlResourceFileName
* file name
* @return top level document
*/
public static Document getDocument(final String xmlResourceFileName) {
final InputStream is = ClassLoader.getSystemResourceAsStream(xmlResourceFileName);
if (is == null) {
throw new RapidBeansRuntimeException("System resource file \"" + xmlResourceFileName + "\" not found");
}
return (Document) getDocumentTopLevel(is, false);
}
/**
* search for the top level document.
*
* @param xmlResourceFile
* the file to load.
* @return top level document
*/
public static Document getDocument(final File xmlResourceFile) {
InputStream is;
try {
is = new FileInputStream(xmlResourceFile);
} catch (FileNotFoundException e) {
throw new RapidBeansRuntimeException(e);
}
return (Document) getDocumentTopLevel(is, false);
}
/**
* search for the top level document.
*
* @param xmlResourceFileName
* file name
* @return top level document
*/
public static Node getDocumentTopLevel(final String xmlResourceFileName) {
final InputStream is = ClassLoader.getSystemResourceAsStream(xmlResourceFileName);
if (is == null) {
throw new RapidBeansRuntimeException("System resource file \"" + xmlResourceFileName + "\" not found");
}
return getDocumentTopLevel(is, true);
}
/**
* search for the top level document.
*
* @param xmlResourceFile
* the file to load.
* @return top level document
*/
public static Node getDocumentTopLevel(final File xmlResourceFile) {
InputStream is;
try {
is = new FileInputStream(xmlResourceFile);
} catch (FileNotFoundException e) {
throw new RapidBeansRuntimeException(e);
}
return getDocumentTopLevel(is, true);
}
/**
* Load the top level document.
*
* @param InputStream
* is
* @return top level document
*/
public static Node getDocumentTopLevel(final InputStream is, final boolean firstChild) {
try {
final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(false);
dbf.setValidating(false);
final DocumentBuilder db = dbf.newDocumentBuilder();
final Document doc = db.parse(is);
if (firstChild) {
return doc.getFirstChild();
}
return doc;
} catch (ParserConfigurationException e) {
throw new RapidBeansRuntimeException(e);
} catch (SAXException e) {
throw new RapidBeansRuntimeException(e);
} catch (IOException e) {
throw new RapidBeansRuntimeException(e);
}
}
public static final SubnodeFilter createSubnodeFilter(final XmlHelper encInstance, final String s) {
if (s.matches(".*\\[[0-9]*]")) {
return encInstance.new SubnodeFilterOrder(s);
} else if (s.matches(".*\\[@.* *= *'.*'].*")) {
return encInstance.new SubnodeFilterAttvals(s);
} else if (s.matches(".*\\[\\. *= *'.*'].*")) {
return encInstance.new SubnodeFilterNodeval(s);
} else {
return encInstance.new SubnodeFilterFirst(s);
}
}
private abstract class SubnodeFilter {
public abstract Node filter(Node[] subnodes, String subnodePattern);
}
private class SubnodeFilterFirst extends SubnodeFilter {
public Node filter(Node[] subnodes, final String subnodePathPattern) {
Node ret;
for (int i = 0; i < subnodes.length; i++) {
ret = getNode(subnodes[i], subnodePathPattern);
if (ret != null) {
return ret;
}
}
return null;
}
public SubnodeFilterFirst(String s) {
if (s.indexOf('[') != -1) {
throw new RapidBeansRuntimeException("'[' found in string \"" + s + "\"");
}
}
}
private class SubnodeFilterOrder extends SubnodeFilter {
private int order = -1;
public Node filter(Node[] subnodes, final String subnodePathPattern) {
if (this.order >= subnodes.length) {
return null;
}
return getNode(subnodes[this.order], subnodePathPattern);
}
public SubnodeFilterOrder(String s) {
if (s.indexOf('[') > -1) {
this.order = Integer.parseInt(s.substring(s.indexOf('[') + 1, s.length() - 1));
} else {
throw new RapidBeansRuntimeException("'[' not found in string \"" + s + "\"");
}
}
}
private class SubnodeFilterNodeval extends SubnodeFilter {
private String nodeval = null;
public Node filter(Node[] subnodes, final String subnodePathPattern) {
Node ret = null;
for (int i = 0; i < subnodes.length; i++) {
if (subnodes[i].getFirstChild().getNodeValue().equals(this.nodeval)) {
ret = getNode(subnodes[i], subnodePathPattern);
break;
}
}
return ret;
}
public SubnodeFilterNodeval(final String s) {
if (s.indexOf('[') > -1) {
this.nodeval = parseNodeval(s);
} else {
throw new RapidBeansRuntimeException("'[' not found in string \"" + s + "\"");
}
}
}
private class SubnodeFilterAttvals extends SubnodeFilter {
private Map<String, String> attrmap = null;
public Node filter(Node[] subnodes, final String subnodePathPattern) {
Node ret = null;
for (int i = 0; i < subnodes.length; i++) {
final NamedNodeMap attrs = subnodes[i].getAttributes();
if (attrs != null) {
boolean match = true;
for (final Entry<String, String> entry : this.attrmap.entrySet()) {
final Node attr = attrs.getNamedItem(entry.getKey());
if ((attr == null) || (!attr.getNodeValue().equals(entry.getValue()))) {
match = false;
break;
}
}
if (match) {
ret = getNode(subnodes[i], subnodePathPattern);
break;
}
}
}
return ret;
}
public SubnodeFilterAttvals(final String s) {
if (s.indexOf('[') > -1) {
this.attrmap = parseIdAttrs(s);
} else {
throw new RapidBeansRuntimeException("'[' not found in string \"" + s + "\"");
}
}
}
public static Map<String, String> parseIdAttrs(final String nodePath) {
final Map<String, String> idAttrs = new HashMap<String, String>();
if (nodePath.contains("[")) {
if (nodePath.matches(".*\\[.*\\]\\z")) {
final String idAttrString = nodePath.substring(nodePath.indexOf('[') + 1, nodePath.length() - 1);
final int len = idAttrString.length();
int state = 0;
String idAttrName = null;
String idAttrValue = null;
StringBuffer buf = new StringBuffer();
for (int i = 0; i < len && state >= 0; i++) {
final char c = idAttrString.charAt(i);
switch (state) {
case 0:
switch (c) {
case ' ':
case '\t':
case '\n':
break;
case '@':
state = 1;
break;
default:
state = -1;
break;
}
break;
case 1:
switch (c) {
case ' ':
case '\t':
case '\n':
idAttrName = buf.toString();
buf.setLength(0);
state = 2;
break;
case '=':
idAttrName = buf.toString();
buf.setLength(0);
state = 3;
break;
default:
buf.append(c);
break;
}
break;
case 2:
switch (c) {
case ' ':
case '\t':
case '\n':
break;
case '=':
state = 3;
break;
default:
state = -1;
break;
}
break;
case 3:
switch (c) {
case ' ':
case '\t':
case '\n':
break;
case '\'':
state = 4;
break;
default:
state = -1;
break;
}
break;
case 4:
switch (c) {
case '\'':
idAttrValue = buf.toString();
idAttrs.put(idAttrName, idAttrValue);
state = 5;
break;
default:
buf.append(c);
break;
}
break;
case 5:
switch (c) {
case ' ':
case '\t':
case '\n':
break;
case 'a':
state = 6;
break;
default:
state = -1;
break;
}
break;
case 6:
switch (c) {
case 'n':
state = 7;
break;
default:
state = -1;
break;
}
break;
case 7:
switch (c) {
case 'd':
state = 0;
break;
default:
state = -1;
break;
}
break;
}
}
if (state == -1) {
idAttrs.clear();
}
}
}
return idAttrs;
}
public static String parseNodeval(final String nodePath) {
final StringBuilder sb = new StringBuilder();
if (nodePath.contains("[")) {
if (nodePath.matches(".*\\[.*\\]\\z")) {
final String idAttrString = nodePath.substring(nodePath.indexOf('[') + 1, nodePath.length() - 1);
final int len = idAttrString.length();
int state = 0;
for (int i = 0; i < len && state >= 0; i++) {
final char c = idAttrString.charAt(i);
switch (state) {
case 0:
switch (c) {
case '.':
case ' ':
case '\t':
case '\n':
break;
case '=':
state = 1;
break;
default:
state = -1;
break;
}
break;
case 1:
switch (c) {
case ' ':
case '\t':
case '\n':
break;
case '\'':
state = 2;
break;
default:
state = -1;
break;
}
break;
case 2:
switch (c) {
case '\'':
state = 5;
break;
default:
sb.append(c);
break;
}
break;
case 5:
switch (c) {
case ' ':
case '\t':
case '\n':
break;
default:
state = -1;
break;
}
break;
}
if (state == -1) {
throw new UtilException("parseNodeval(\"" + nodePath + "\") failed with invalid parser state");
}
}
}
}
return sb.toString();
}
}