View Javadoc
1   /*
2    * Copyright 2004-2025 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *    https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package com.ibatis.common.xml;
17  
18  import java.io.IOException;
19  import java.io.InputStream;
20  import java.io.Reader;
21  import java.util.*;
22  
23  import javax.xml.XMLConstants;
24  import javax.xml.parsers.DocumentBuilder;
25  import javax.xml.parsers.DocumentBuilderFactory;
26  import javax.xml.parsers.FactoryConfigurationError;
27  import javax.xml.parsers.ParserConfigurationException;
28  
29  import org.w3c.dom.*;
30  import org.xml.sax.*;
31  
32  /**
33   * The NodeletParser is a callback based parser similar to SAX. The big difference is that rather than having a single
34   * callback for all nodes, the NodeletParser has a number of callbacks mapped to various nodes. The callback is called a
35   * Nodelet and it is registered with the NodeletParser against a specific XPath.
36   */
37  public class NodeletParser {
38  
39    /** The let map. */
40    private Map letMap = new HashMap();
41  
42    /** The validation. */
43    private boolean validation;
44  
45    /** The entity resolver. */
46    private EntityResolver entityResolver;
47  
48    /**
49     * Registers a nodelet for the specified XPath. Current XPaths supported are:
50     * <ul>
51     * <li>Text Path - /rootElement/childElement/text()
52     * <li>Attribute Path - /rootElement/childElement/@theAttribute
53     * <li>Element Path - /rootElement/childElement/theElement
54     * <li>All Elements Named - //theElement
55     * </ul>
56     *
57     * @param xpath
58     *          the xpath
59     * @param nodelet
60     *          the nodelet
61     */
62    public void addNodelet(String xpath, Nodelet nodelet) {
63      letMap.put(xpath, nodelet);
64    }
65  
66    /**
67     * Begins parsing from the provided Reader.
68     *
69     * @param reader
70     *          the reader
71     *
72     * @throws NodeletException
73     *           the nodelet exception
74     */
75    public void parse(Reader reader) throws NodeletException {
76      try {
77        Document doc = createDocument(reader);
78        parse(doc.getLastChild());
79      } catch (Exception e) {
80        throw new NodeletException("Error parsing XML.  Cause: " + e, e);
81      }
82    }
83  
84    /**
85     * Parses the.
86     *
87     * @param inputStream
88     *          the input stream
89     *
90     * @throws NodeletException
91     *           the nodelet exception
92     */
93    public void parse(InputStream inputStream) throws NodeletException {
94      try {
95        Document doc = createDocument(inputStream);
96        parse(doc.getLastChild());
97      } catch (Exception e) {
98        throw new NodeletException("Error parsing XML.  Cause: " + e, e);
99      }
100   }
101 
102   /**
103    * Begins parsing from the provided Node.
104    *
105    * @param node
106    *          the node
107    */
108   public void parse(Node node) {
109     Path path = new Path();
110     processNodelet(node, "/");
111     process(node, path);
112   }
113 
114   /**
115    * A recursive method that walkes the DOM tree, registers XPaths and calls Nodelets registered under those XPaths.
116    *
117    * @param node
118    *          the node
119    * @param path
120    *          the path
121    */
122   private void process(Node node, Path path) {
123     if (node instanceof Element) {
124       // Element
125       String elementName = node.getNodeName();
126       path.add(elementName);
127       processNodelet(node, path.toString());
128       processNodelet(node, new StringBuilder("//").append(elementName).toString());
129 
130       // Attribute
131       NamedNodeMap attributes = node.getAttributes();
132       int n = attributes.getLength();
133       for (int i = 0; i < n; i++) {
134         Node att = attributes.item(i);
135         String attrName = att.getNodeName();
136         path.add("@" + attrName);
137         processNodelet(att, path.toString());
138         processNodelet(node, new StringBuilder("//@").append(attrName).toString());
139         path.remove();
140       }
141 
142       // Children
143       NodeList children = node.getChildNodes();
144       for (int i = 0; i < children.getLength(); i++) {
145         process(children.item(i), path);
146       }
147       path.add("end()");
148       processNodelet(node, path.toString());
149       path.remove();
150       path.remove();
151     } else if (node instanceof Text) {
152       // Text
153       path.add("text()");
154       processNodelet(node, path.toString());
155       processNodelet(node, "//text()");
156       path.remove();
157     }
158   }
159 
160   /**
161    * Process nodelet.
162    *
163    * @param node
164    *          the node
165    * @param pathString
166    *          the path string
167    */
168   private void processNodelet(Node node, String pathString) {
169     Nodelet nodelet = (Nodelet) letMap.get(pathString);
170     if (nodelet != null) {
171       try {
172         nodelet.process(node);
173       } catch (Exception e) {
174         throw new RuntimeException("Error parsing XPath '" + pathString + "'.  Cause: " + e, e);
175       }
176     }
177   }
178 
179   /**
180    * Creates a JAXP Document from a reader.
181    *
182    * @param reader
183    *          the reader
184    *
185    * @return the document
186    *
187    * @throws ParserConfigurationException
188    *           the parser configuration exception
189    * @throws FactoryConfigurationError
190    *           the factory configuration error
191    * @throws SAXException
192    *           the SAX exception
193    * @throws IOException
194    *           Signals that an I/O exception has occurred.
195    */
196   private Document createDocument(Reader reader)
197       throws ParserConfigurationException, FactoryConfigurationError, SAXException, IOException {
198     DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
199     factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
200     factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
201     factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
202     factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
203     factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
204     factory.setValidating(validation);
205 
206     factory.setNamespaceAware(false);
207     factory.setIgnoringComments(true);
208     factory.setIgnoringElementContentWhitespace(false);
209     factory.setCoalescing(false);
210     factory.setExpandEntityReferences(false);
211 
212     DocumentBuilder builder = factory.newDocumentBuilder();
213     builder.setEntityResolver(entityResolver);
214     builder.setErrorHandler(new ErrorHandler() {
215       public void error(SAXParseException exception) throws SAXException {
216         throw exception;
217       }
218 
219       public void fatalError(SAXParseException exception) throws SAXException {
220         throw exception;
221       }
222 
223       public void warning(SAXParseException exception) throws SAXException {
224       }
225     });
226 
227     return builder.parse(new InputSource(reader));
228   }
229 
230   /**
231    * Creates a JAXP Document from an InoutStream.
232    *
233    * @param inputStream
234    *          the input stream
235    *
236    * @return the document
237    *
238    * @throws ParserConfigurationException
239    *           the parser configuration exception
240    * @throws FactoryConfigurationError
241    *           the factory configuration error
242    * @throws SAXException
243    *           the SAX exception
244    * @throws IOException
245    *           Signals that an I/O exception has occurred.
246    */
247   private Document createDocument(InputStream inputStream)
248       throws ParserConfigurationException, FactoryConfigurationError, SAXException, IOException {
249     DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
250     factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
251     factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
252     factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
253     factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
254     factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
255     factory.setValidating(validation);
256 
257     factory.setNamespaceAware(false);
258     factory.setIgnoringComments(true);
259     factory.setIgnoringElementContentWhitespace(false);
260     factory.setCoalescing(false);
261     factory.setExpandEntityReferences(false);
262 
263     DocumentBuilder builder = factory.newDocumentBuilder();
264     builder.setEntityResolver(entityResolver);
265     builder.setErrorHandler(new ErrorHandler() {
266       public void error(SAXParseException exception) throws SAXException {
267         throw exception;
268       }
269 
270       public void fatalError(SAXParseException exception) throws SAXException {
271         throw exception;
272       }
273 
274       public void warning(SAXParseException exception) throws SAXException {
275       }
276     });
277 
278     return builder.parse(new InputSource(inputStream));
279   }
280 
281   /**
282    * Sets the validation.
283    *
284    * @param validation
285    *          the new validation
286    */
287   public void setValidation(boolean validation) {
288     this.validation = validation;
289   }
290 
291   /**
292    * Sets the entity resolver.
293    *
294    * @param resolver
295    *          the new entity resolver
296    */
297   public void setEntityResolver(EntityResolver resolver) {
298     this.entityResolver = resolver;
299   }
300 
301   /**
302    * Inner helper class that assists with building XPath paths.
303    * <p>
304    * Note: Currently this is a bit slow and could be optimized.
305    */
306   private static class Path {
307 
308     /** The node list. */
309     private List nodeList = new ArrayList();
310 
311     /**
312      * Instantiates a new path.
313      */
314     public Path() {
315     }
316 
317     /**
318      * Instantiates a new path.
319      *
320      * @param path
321      *          the path
322      */
323     public Path(String path) {
324       StringTokenizer parser = new StringTokenizer(path, "/", false);
325       while (parser.hasMoreTokens()) {
326         nodeList.add(parser.nextToken());
327       }
328     }
329 
330     /**
331      * Adds the.
332      *
333      * @param node
334      *          the node
335      */
336     public void add(String node) {
337       nodeList.add(node);
338     }
339 
340     /**
341      * Removes the.
342      */
343     public void remove() {
344       nodeList.remove(nodeList.size() - 1);
345     }
346 
347     @Override
348     public String toString() {
349       StringBuilder builder = new StringBuilder("/");
350       for (int i = 0; i < nodeList.size(); i++) {
351         builder.append(nodeList.get(i));
352         if (i < nodeList.size() - 1) {
353           builder.append("/");
354         }
355       }
356       return builder.toString();
357     }
358   }
359 
360 }