package org.yy.mongodb.orm.builder; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.FactoryConfigurationError; import javax.xml.parsers.ParserConfigurationException; import org.springframework.core.NestedIOException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.EntityResolver; import org.xml.sax.ErrorHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.yy.mongodb.constant.ORM; /** * XML node parser. * @author yy */ public class NodeletParser { private Map<String, Nodelet> letMap = new HashMap<String, Nodelet>(); private boolean validation; private EntityResolver entityResolver; /** * Registers a nodelet for the specified XPath. Current XPaths supported * are: * <ul> * <li> Attribute Path - /rootElement/childElement/@theAttribute * <li> Element Path - /rootElement/childElement/theElement * <li> All Elements Named - //theElement * </ul> */ public void addNodelet(String xpath, Nodelet nodelet) { letMap.put(xpath, nodelet); } /** * Begins parsing from the provided Reader. */ public void parse(Reader reader) throws NestedIOException { try { Document doc = createDocument(reader); parse(doc.getLastChild()); } catch (Exception e) { throw new NestedIOException("Error parsing XML. Cause: " + e, e); } } public void parse(InputStream inputStream) throws NestedIOException { try { Document doc = createDocument(inputStream); parse(doc.getLastChild()); } catch (Exception e) { throw new NestedIOException("Error parsing XML. Cause: " + e, e); } } /** * Begins parsing from the provided Node. */ public void parse(Node node) { Path path = new Path(); process(node, null, path); } /** * A recursive method that walkes the DOM tree, registers XPaths and * calls Nodelets registered under those XPaths. */ private void process(Node node, String namespace, Path path) { if (node instanceof Element) { // Element String elementName = node.getNodeName(); if (namespace == null && "mql".equals(elementName)) { namespace = ((Element) node).getAttribute(ORM.TAG_NAMESPACE); } path.add(elementName); processNodelet(node, namespace, path.toString()); processNodelet(node, namespace, new StringBuffer("//").append(elementName).toString()); // Children NodeList children = node.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { process(children.item(i), namespace, path); } path.remove(); } } private void processNodelet(Node node, String namespace, String pathString) { Nodelet nodelet = (Nodelet) letMap.get(pathString); if (nodelet != null) { try { nodelet.process(namespace, node); } catch (Exception e) { throw new RuntimeException("Error parsing XPath '" + pathString + "'. Cause: " + e, e); } } } /** * Creates a JAXP Document from a reader. */ private Document createDocument(Reader reader) throws ParserConfigurationException, FactoryConfigurationError, SAXException, IOException { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(validation); factory.setIgnoringComments(true); factory.setIgnoringElementContentWhitespace(true); DocumentBuilder builder = factory.newDocumentBuilder(); builder.setEntityResolver(entityResolver); builder.setErrorHandler(new ErrorHandler() { public void error(SAXParseException exception) throws SAXException { throw exception; } public void fatalError(SAXParseException exception) throws SAXException { throw exception; } public void warning(SAXParseException exception) throws SAXException { } }); return builder.parse(new InputSource(reader)); } /** * Creates a JAXP Document from an InoutStream. */ private Document createDocument(InputStream inputStream) throws ParserConfigurationException, FactoryConfigurationError, SAXException, IOException { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(validation); factory.setIgnoringElementContentWhitespace(true); factory.setIgnoringComments(true); DocumentBuilder builder = factory.newDocumentBuilder(); builder.setEntityResolver(entityResolver); builder.setErrorHandler(new ErrorHandler() { public void error(SAXParseException exception) throws SAXException { throw exception; } public void fatalError(SAXParseException exception) throws SAXException { throw exception; } public void warning(SAXParseException exception) throws SAXException { } }); InputSource input = new InputSource(inputStream); input.setEncoding("UTF-8"); return builder.parse(input); } public void setValidation(boolean validation) { this.validation = validation; } public void setEntityResolver(EntityResolver resolver) { this.entityResolver = resolver; } /** * Inner helper class that assists with building XPath paths. * <p/> * Note: Currently this is a bit slow and could be optimized. */ private static class Path { private List<String> nodeList = new ArrayList<String>(); public void add(String node) { nodeList.add(node); } public void remove() { nodeList.remove(nodeList.size() - 1); } public String toString() { StringBuilder builder = new StringBuilder("/"); for (int i = 0; i < nodeList.size(); i++) { builder.append(nodeList.get(i)); if (i < nodeList.size() - 1) { builder.append("/"); } } return builder.toString(); } } }