/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.camel.parser.helper; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.util.Stack; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; /** * An XML parser that uses SAX to include line and column number for each XML element in the parsed Document. * <p/> * The line number and column number can be obtained from a Node/Element using * <pre> * String lineNumber = (String) node.getUserData(XmlLineNumberParser.LINE_NUMBER); * String lineNumberEnd = (String) node.getUserData(XmlLineNumberParser.LINE_NUMBER_END); * String columnNumber = (String) node.getUserData(XmlLineNumberParser.COLUMN_NUMBER); * String columnNumberEnd = (String) node.getUserData(XmlLineNumberParser.COLUMN_NUMBER_END); * </pre> */ public final class XmlLineNumberParser { public static final String LINE_NUMBER = "lineNumber"; public static final String COLUMN_NUMBER = "colNumber"; public static final String LINE_NUMBER_END = "lineNumberEnd"; public static final String COLUMN_NUMBER_END = "colNumberEnd"; private XmlLineNumberParser() { } /** * Parses the XML. * * @param is the XML content as an input stream * @return the DOM model * @throws Exception is thrown if error parsing */ public static Document parseXml(final InputStream is) throws Exception { return parseXml(is, null, null); } /** * Parses the XML. * * @param is the XML content as an input stream * @param rootNames one or more root names that is used as baseline for beginning the parsing, for example camelContext to start parsing * when Camel is discovered. Multiple names can be defined separated by comma * @param forceNamespace an optional namespace to force assign to each node. This may be needed for JAXB unmarshalling from XML -> POJO. * @return the DOM model * @throws Exception is thrown if error parsing */ public static Document parseXml(final InputStream is, final String rootNames, final String forceNamespace) throws Exception { final Document doc; SAXParser parser; final SAXParserFactory factory = SAXParserFactory.newInstance(); parser = factory.newSAXParser(); final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); // turn off validator and loading external dtd dbf.setValidating(false); dbf.setNamespaceAware(true); dbf.setFeature("http://xml.org/sax/features/namespaces", false); dbf.setFeature("http://xml.org/sax/features/validation", false); dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); final DocumentBuilder docBuilder = dbf.newDocumentBuilder(); doc = docBuilder.newDocument(); final Stack<Element> elementStack = new Stack<Element>(); final StringBuilder textBuffer = new StringBuilder(); final DefaultHandler handler = new DefaultHandler() { private Locator locator; private boolean found; @Override public void setDocumentLocator(final Locator locator) { this.locator = locator; // Save the locator, so that it can be used later for line tracking when traversing nodes. this.found = rootNames == null; } private boolean isRootName(String qName) { for (String root : rootNames.split(",")) { if (qName.equals(root)) { return true; } } return false; } @Override public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException { addTextIfNeeded(); if (rootNames != null && !found) { if (isRootName(qName)) { found = true; } } if (found) { Element el; if (forceNamespace != null) { el = doc.createElementNS(forceNamespace, qName); } else { el = doc.createElement(qName); } for (int i = 0; i < attributes.getLength(); i++) { el.setAttribute(attributes.getQName(i), attributes.getValue(i)); } el.setUserData(LINE_NUMBER, String.valueOf(this.locator.getLineNumber()), null); el.setUserData(COLUMN_NUMBER, String.valueOf(this.locator.getColumnNumber()), null); elementStack.push(el); } } @Override public void endElement(final String uri, final String localName, final String qName) { if (!found) { return; } addTextIfNeeded(); final Element closedEl = elementStack.isEmpty() ? null : elementStack.pop(); if (closedEl != null) { if (elementStack.isEmpty()) { // Is this the root element? doc.appendChild(closedEl); } else { final Element parentEl = elementStack.peek(); parentEl.appendChild(closedEl); } closedEl.setUserData(LINE_NUMBER_END, String.valueOf(this.locator.getLineNumber()), null); closedEl.setUserData(COLUMN_NUMBER_END, String.valueOf(this.locator.getColumnNumber()), null); } } @Override public void characters(final char ch[], final int start, final int length) throws SAXException { textBuffer.append(ch, start, length); } @Override public InputSource resolveEntity(String publicId, String systemId) throws IOException, SAXException { // do not resolve external dtd return new InputSource(new StringReader("")); } // Outputs text accumulated under the current node private void addTextIfNeeded() { if (textBuffer.length() > 0) { final Element el = elementStack.isEmpty() ? null : elementStack.peek(); if (el != null) { final Node textNode = doc.createTextNode(textBuffer.toString()); el.appendChild(textNode); textBuffer.delete(0, textBuffer.length()); } } } }; parser.parse(is, handler); return doc; } }