View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.xml;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.net.URL;
25  import java.util.AbstractList;
26  import java.util.ArrayList;
27  import java.util.HashMap;
28  import java.util.Iterator;
29  import java.util.Map;
30  import java.util.NoSuchElementException;
31  import java.util.Stack;
32  import java.util.StringTokenizer;
33  
34  import javax.xml.parsers.SAXParser;
35  import javax.xml.parsers.SAXParserFactory;
36  
37  import org.eclipse.jetty.util.LazyList;
38  import org.eclipse.jetty.util.log.Log;
39  import org.eclipse.jetty.util.log.Logger;
40  import org.eclipse.jetty.util.resource.Resource;
41  import org.xml.sax.Attributes;
42  import org.xml.sax.ContentHandler;
43  import org.xml.sax.InputSource;
44  import org.xml.sax.SAXException;
45  import org.xml.sax.SAXParseException;
46  import org.xml.sax.XMLReader;
47  import org.xml.sax.helpers.DefaultHandler;
48  
49  /*--------------------------------------------------------------*/
50  /**
51   * XML Parser wrapper. This class wraps any standard JAXP1.1 parser with convieniant error and
52   * entity handlers and a mini dom-like document tree.
53   * <P>
54   * By default, the parser is created as a validating parser only if xerces is present. This can be
55   * configured by setting the "org.eclipse.jetty.xml.XmlParser.Validating" system property.
56   *
57   *
58   */
59  public class XmlParser
60  {
61      private static final Logger LOG = Log.getLogger(XmlParser.class);
62  
63      private Map<String,URL> _redirectMap = new HashMap<String,URL>();
64      private SAXParser _parser;
65      private Map<String,ContentHandler> _observerMap;
66      private Stack<ContentHandler> _observers = new Stack<ContentHandler>();
67      private String _xpath;
68      private Object _xpaths;
69      private String _dtd;
70  
71      /* ------------------------------------------------------------ */
72      /**
73       * Construct
74       */
75      public XmlParser()
76      {
77          SAXParserFactory factory = SAXParserFactory.newInstance();
78          boolean validating_dft = factory.getClass().toString().startsWith("org.apache.xerces.");
79          String validating_prop = System.getProperty("org.eclipse.jetty.xml.XmlParser.Validating", validating_dft ? "true" : "false");
80          boolean validating = Boolean.valueOf(validating_prop).booleanValue();
81          setValidating(validating);
82      }
83  
84      /* ------------------------------------------------------------ */
85      /**
86       * Constructor.
87       */
88      public XmlParser(boolean validating)
89      {
90          setValidating(validating);
91      }
92  
93      /* ------------------------------------------------------------ */
94      public void setValidating(boolean validating)
95      {
96          try
97          {
98              SAXParserFactory factory = SAXParserFactory.newInstance();
99              factory.setValidating(validating);
100             _parser = factory.newSAXParser();
101 
102             try
103             {
104                 if (validating)
105                     _parser.getXMLReader().setFeature("http://apache.org/xml/features/validation/schema", validating);
106             }
107             catch (Exception e)
108             {
109                 if (validating)
110                     LOG.warn("Schema validation may not be supported: ", e);
111                 else
112                     LOG.ignore(e);
113             }
114 
115             _parser.getXMLReader().setFeature("http://xml.org/sax/features/validation", validating);
116             _parser.getXMLReader().setFeature("http://xml.org/sax/features/namespaces", true);
117             _parser.getXMLReader().setFeature("http://xml.org/sax/features/namespace-prefixes", false);
118             try
119             {
120                 if (validating)
121                     _parser.getXMLReader().setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", validating);
122             }
123             catch (Exception e)
124             {
125                 LOG.warn(e.getMessage());
126             }
127         }
128         catch (Exception e)
129         {
130             LOG.warn(Log.EXCEPTION, e);
131             throw new Error(e.toString());
132         }
133     }
134 
135     /* ------------------------------------------------------------ */
136     /**
137      * @param name
138      * @param entity
139      */
140     public synchronized void redirectEntity(String name, URL entity)
141     {
142         if (entity != null)
143             _redirectMap.put(name, entity);
144     }
145 
146     /* ------------------------------------------------------------ */
147     /**
148      *
149      * @return Returns the xpath.
150      */
151     public String getXpath()
152     {
153         return _xpath;
154     }
155 
156     /* ------------------------------------------------------------ */
157     /**
158      * Set an XPath A very simple subset of xpath is supported to select a partial tree. Currently
159      * only path like "/node1/nodeA | /node1/nodeB" are supported.
160      *
161      * @param xpath The xpath to set.
162      */
163     public void setXpath(String xpath)
164     {
165         _xpath = xpath;
166         StringTokenizer tok = new StringTokenizer(xpath, "| ");
167         while (tok.hasMoreTokens())
168             _xpaths = LazyList.add(_xpaths, tok.nextToken());
169     }
170 
171     /* ------------------------------------------------------------ */
172     public String getDTD()
173     {
174         return _dtd;
175     }
176 
177     /* ------------------------------------------------------------ */
178     /**
179      * Add a ContentHandler. Add an additional _content handler that is triggered on a tag name. SAX
180      * events are passed to the ContentHandler provided from a matching start element to the
181      * corresponding end element. Only a single _content handler can be registered against each tag.
182      *
183      * @param trigger Tag local or q name.
184      * @param observer SAX ContentHandler
185      */
186     public synchronized void addContentHandler(String trigger, ContentHandler observer)
187     {
188         if (_observerMap == null)
189             _observerMap = new HashMap<>();
190         _observerMap.put(trigger, observer);
191     }
192 
193     /* ------------------------------------------------------------ */
194     public synchronized Node parse(InputSource source) throws IOException, SAXException
195     {
196         _dtd=null;
197         Handler handler = new Handler();
198         XMLReader reader = _parser.getXMLReader();
199         reader.setContentHandler(handler);
200         reader.setErrorHandler(handler);
201         reader.setEntityResolver(handler);
202         if (LOG.isDebugEnabled())
203             LOG.debug("parsing: sid=" + source.getSystemId() + ",pid=" + source.getPublicId());
204         _parser.parse(source, handler);
205         if (handler._error != null)
206             throw handler._error;
207         Node doc = (Node) handler._top.get(0);
208         handler.clear();
209         return doc;
210     }
211 
212     /* ------------------------------------------------------------ */
213     /**
214      * Parse String URL.
215      */
216     public synchronized Node parse(String url) throws IOException, SAXException
217     {
218         if (LOG.isDebugEnabled())
219             LOG.debug("parse: " + url);
220         return parse(new InputSource(url));
221     }
222 
223     /* ------------------------------------------------------------ */
224     /**
225      * Parse File.
226      */
227     public synchronized Node parse(File file) throws IOException, SAXException
228     {
229         if (LOG.isDebugEnabled())
230             LOG.debug("parse: " + file);
231         return parse(new InputSource(Resource.toURL(file).toString()));
232     }
233 
234     /* ------------------------------------------------------------ */
235     /**
236      * Parse InputStream.
237      */
238     public synchronized Node parse(InputStream in) throws IOException, SAXException
239     {
240         _dtd=null;
241         Handler handler = new Handler();
242         XMLReader reader = _parser.getXMLReader();
243         reader.setContentHandler(handler);
244         reader.setErrorHandler(handler);
245         reader.setEntityResolver(handler);
246         _parser.parse(new InputSource(in), handler);
247         if (handler._error != null)
248             throw handler._error;
249         Node doc = (Node) handler._top.get(0);
250         handler.clear();
251         return doc;
252     }
253 
254 
255     /* ------------------------------------------------------------ */
256     protected InputSource resolveEntity(String pid, String sid)
257     {
258         if (LOG.isDebugEnabled())
259             LOG.debug("resolveEntity(" + pid + ", " + sid + ")");
260 
261         if (sid!=null && sid.endsWith(".dtd"))
262             _dtd=sid;
263 
264         URL entity = null;
265         if (pid != null)
266             entity = (URL) _redirectMap.get(pid);
267         if (entity == null)
268             entity = (URL) _redirectMap.get(sid);
269         if (entity == null)
270         {
271             String dtd = sid;
272             if (dtd.lastIndexOf('/') >= 0)
273                 dtd = dtd.substring(dtd.lastIndexOf('/') + 1);
274 
275             if (LOG.isDebugEnabled())
276                 LOG.debug("Can't exact match entity in redirect map, trying " + dtd);
277             entity = (URL) _redirectMap.get(dtd);
278         }
279 
280         if (entity != null)
281         {
282             try
283             {
284                 InputStream in = entity.openStream();
285                 if (LOG.isDebugEnabled())
286                     LOG.debug("Redirected entity " + sid + " --> " + entity);
287                 InputSource is = new InputSource(in);
288                 is.setSystemId(sid);
289                 return is;
290             }
291             catch (IOException e)
292             {
293                 LOG.ignore(e);
294             }
295         }
296         return null;
297     }
298     
299     /* ------------------------------------------------------------ */
300     /* ------------------------------------------------------------ */
301     private class NoopHandler extends DefaultHandler
302     {
303         Handler _next;
304         int _depth;
305 
306         NoopHandler(Handler next)
307         {
308             this._next = next;
309         }
310 
311         /* ------------------------------------------------------------ */
312         public void startElement(String uri, String localName, String qName, Attributes attrs) throws SAXException
313         {
314             _depth++;
315         }
316 
317         /* ------------------------------------------------------------ */
318         public void endElement(String uri, String localName, String qName) throws SAXException
319         {
320             if (_depth == 0)
321                 _parser.getXMLReader().setContentHandler(_next);
322             else
323                 _depth--;
324         }
325     }
326 
327     /* ------------------------------------------------------------ */
328     /* ------------------------------------------------------------ */
329     private class Handler extends DefaultHandler
330     {
331         Node _top = new Node(null, null, null);
332         SAXParseException _error;
333         private Node _context = _top;
334         private NoopHandler _noop;
335 
336         Handler()
337         {
338             _noop = new NoopHandler(this);
339         }
340 
341         /* ------------------------------------------------------------ */
342         void clear()
343         {
344             _top = null;
345             _error = null;
346             _context = null;
347         }
348 
349         /* ------------------------------------------------------------ */
350         public void startElement(String uri, String localName, String qName, Attributes attrs) throws SAXException
351         {
352             String name = null;
353             if (_parser.isNamespaceAware())
354                 name = localName;
355 
356             if (name == null || "".equals(name))
357                 name = qName;
358 
359             Node node = new Node(_context, name, attrs);
360 
361 
362             // check if the node matches any xpaths set?
363             if (_xpaths != null)
364             {
365                 String path = node.getPath();
366                 boolean match = false;
367                 for (int i = LazyList.size(_xpaths); !match && i-- > 0;)
368                 {
369                     String xpath = (String) LazyList.get(_xpaths, i);
370 
371                     match = path.equals(xpath) || xpath.startsWith(path) && xpath.length() > path.length() && xpath.charAt(path.length()) == '/';
372                 }
373 
374                 if (match)
375                 {
376                     _context.add(node);
377                     _context = node;
378                 }
379                 else
380                 {
381                     _parser.getXMLReader().setContentHandler(_noop);
382                 }
383             }
384             else
385             {
386                 _context.add(node);
387                 _context = node;
388             }
389 
390             ContentHandler observer = null;
391             if (_observerMap != null)
392                 observer = (ContentHandler) _observerMap.get(name);
393             _observers.push(observer);
394 
395             for (int i = 0; i < _observers.size(); i++)
396                 if (_observers.get(i) != null)
397                     ((ContentHandler) _observers.get(i)).startElement(uri, localName, qName, attrs);
398         }
399 
400         /* ------------------------------------------------------------ */
401         public void endElement(String uri, String localName, String qName) throws SAXException
402         {
403             _context = _context._parent;
404             for (int i = 0; i < _observers.size(); i++)
405                 if (_observers.get(i) != null)
406                     ((ContentHandler) _observers.get(i)).endElement(uri, localName, qName);
407             _observers.pop();
408         }
409 
410         /* ------------------------------------------------------------ */
411         public void ignorableWhitespace(char buf[], int offset, int len) throws SAXException
412         {
413             for (int i = 0; i < _observers.size(); i++)
414                 if (_observers.get(i) != null)
415                     ((ContentHandler) _observers.get(i)).ignorableWhitespace(buf, offset, len);
416         }
417 
418         /* ------------------------------------------------------------ */
419         public void characters(char buf[], int offset, int len) throws SAXException
420         {
421             _context.add(new String(buf, offset, len));
422             for (int i = 0; i < _observers.size(); i++)
423                 if (_observers.get(i) != null)
424                     ((ContentHandler) _observers.get(i)).characters(buf, offset, len);
425         }
426 
427         /* ------------------------------------------------------------ */
428         public void warning(SAXParseException ex)
429         {
430             LOG.debug(Log.EXCEPTION, ex);
431             LOG.warn("WARNING@" + getLocationString(ex) + " : " + ex.toString());
432         }
433 
434         /* ------------------------------------------------------------ */
435         public void error(SAXParseException ex) throws SAXException
436         {
437             // Save error and continue to report other errors
438             if (_error == null)
439                 _error = ex;
440             LOG.debug(Log.EXCEPTION, ex);
441             LOG.warn("ERROR@" + getLocationString(ex) + " : " + ex.toString());
442         }
443 
444         /* ------------------------------------------------------------ */
445         public void fatalError(SAXParseException ex) throws SAXException
446         {
447             _error = ex;
448             LOG.debug(Log.EXCEPTION, ex);
449             LOG.warn("FATAL@" + getLocationString(ex) + " : " + ex.toString());
450             throw ex;
451         }
452 
453         /* ------------------------------------------------------------ */
454         private String getLocationString(SAXParseException ex)
455         {
456             return ex.getSystemId() + " line:" + ex.getLineNumber() + " col:" + ex.getColumnNumber();
457         }
458 
459         /* ------------------------------------------------------------ */
460         public InputSource resolveEntity(String pid, String sid)
461         {
462             return XmlParser.this.resolveEntity(pid,sid);
463         }
464     }
465 
466     /* ------------------------------------------------------------ */
467     /* ------------------------------------------------------------ */
468     /**
469      * XML Attribute.
470      */
471     public static class Attribute
472     {
473         private String _name;
474         private String _value;
475 
476         Attribute(String n, String v)
477         {
478             _name = n;
479             _value = v;
480         }
481 
482         public String getName()
483         {
484             return _name;
485         }
486 
487         public String getValue()
488         {
489             return _value;
490         }
491     }
492 
493     /* ------------------------------------------------------------ */
494     /* ------------------------------------------------------------ */
495     /**
496      * XML Node. Represents an XML element with optional attributes and ordered content.
497      */
498     public static class Node extends AbstractList<Object>
499     {
500         Node _parent;
501         private ArrayList<Object> _list;
502         private String _tag;
503         private Attribute[] _attrs;
504         private boolean _lastString = false;
505         private String _path;
506 
507         /* ------------------------------------------------------------ */
508         Node(Node parent, String tag, Attributes attrs)
509         {
510             _parent = parent;
511             _tag = tag;
512 
513             if (attrs != null)
514             {
515                 _attrs = new Attribute[attrs.getLength()];
516                 for (int i = 0; i < attrs.getLength(); i++)
517                 {
518                     String name = attrs.getLocalName(i);
519                     if (name == null || name.equals(""))
520                         name = attrs.getQName(i);
521                     _attrs[i] = new Attribute(name, attrs.getValue(i));
522                 }
523             }
524         }
525 
526         /* ------------------------------------------------------------ */
527         public Node getParent()
528         {
529             return _parent;
530         }
531 
532         /* ------------------------------------------------------------ */
533         public String getTag()
534         {
535             return _tag;
536         }
537 
538         /* ------------------------------------------------------------ */
539         public String getPath()
540         {
541             if (_path == null)
542             {
543                 if (getParent() != null && getParent().getTag() != null)
544                     _path = getParent().getPath() + "/" + _tag;
545                 else
546                     _path = "/" + _tag;
547             }
548             return _path;
549         }
550 
551         /* ------------------------------------------------------------ */
552         /**
553          * Get an array of element attributes.
554          */
555         public Attribute[] getAttributes()
556         {
557             return _attrs;
558         }
559 
560         /* ------------------------------------------------------------ */
561         /**
562          * Get an element attribute.
563          *
564          * @return attribute or null.
565          */
566         public String getAttribute(String name)
567         {
568             return getAttribute(name, null);
569         }
570 
571         /* ------------------------------------------------------------ */
572         /**
573          * Get an element attribute.
574          *
575          * @return attribute or null.
576          */
577         public String getAttribute(String name, String dft)
578         {
579             if (_attrs == null || name == null)
580                 return dft;
581             for (int i = 0; i < _attrs.length; i++)
582                 if (name.equals(_attrs[i].getName()))
583                     return _attrs[i].getValue();
584             return dft;
585         }
586 
587         /* ------------------------------------------------------------ */
588         /**
589          * Get the number of children nodes.
590          */
591         public int size()
592         {
593             if (_list != null)
594                 return _list.size();
595             return 0;
596         }
597 
598         /* ------------------------------------------------------------ */
599         /**
600          * Get the ith child node or content.
601          *
602          * @return Node or String.
603          */
604         public Object get(int i)
605         {
606             if (_list != null)
607                 return _list.get(i);
608             return null;
609         }
610 
611         /* ------------------------------------------------------------ */
612         /**
613          * Get the first child node with the tag.
614          *
615          * @param tag
616          * @return Node or null.
617          */
618         public Node get(String tag)
619         {
620             if (_list != null)
621             {
622                 for (int i = 0; i < _list.size(); i++)
623                 {
624                     Object o = _list.get(i);
625                     if (o instanceof Node)
626                     {
627                         Node n = (Node) o;
628                         if (tag.equals(n._tag))
629                             return n;
630                     }
631                 }
632             }
633             return null;
634         }
635 
636         /* ------------------------------------------------------------ */
637         @Override
638         public void add(int i, Object o)
639         {
640             if (_list == null)
641                 _list = new ArrayList<Object>();
642             if (o instanceof String)
643             {
644                 if (_lastString)
645                 {
646                     int last = _list.size() - 1;
647                     _list.set(last, (String) _list.get(last) + o);
648                 }
649                 else
650                     _list.add(i, o);
651                 _lastString = true;
652             }
653             else
654             {
655                 _lastString = false;
656                 _list.add(i, o);
657             }
658         }
659 
660         /* ------------------------------------------------------------ */
661         public void clear()
662         {
663             if (_list != null)
664                 _list.clear();
665             _list = null;
666         }
667 
668         /* ------------------------------------------------------------ */
669         /**
670          * Get a tag as a string.
671          *
672          * @param tag The tag to get
673          * @param tags IF true, tags are included in the value.
674          * @param trim If true, trim the value.
675          * @return results of get(tag).toString(tags).
676          */
677         public String getString(String tag, boolean tags, boolean trim)
678         {
679             Node node = get(tag);
680             if (node == null)
681                 return null;
682             String s = node.toString(tags);
683             if (s != null && trim)
684                 s = s.trim();
685             return s;
686         }
687 
688         /* ------------------------------------------------------------ */
689         public synchronized String toString()
690         {
691             return toString(true);
692         }
693 
694         /* ------------------------------------------------------------ */
695         /**
696          * Convert to a string.
697          *
698          * @param tag If false, only _content is shown.
699          */
700         public synchronized String toString(boolean tag)
701         {
702             StringBuilder buf = new StringBuilder();
703             toString(buf, tag);
704             return buf.toString();
705         }
706 
707         /* ------------------------------------------------------------ */
708         /**
709          * Convert to a string.
710          *
711          * @param tag If false, only _content is shown.
712          */
713         public synchronized String toString(boolean tag, boolean trim)
714         {
715             String s = toString(tag);
716             if (s != null && trim)
717                 s = s.trim();
718             return s;
719         }
720 
721         /* ------------------------------------------------------------ */
722         private synchronized void toString(StringBuilder buf, boolean tag)
723         {
724             if (tag)
725             {
726                 buf.append("<");
727                 buf.append(_tag);
728 
729                 if (_attrs != null)
730                 {
731                     for (int i = 0; i < _attrs.length; i++)
732                     {
733                         buf.append(' ');
734                         buf.append(_attrs[i].getName());
735                         buf.append("=\"");
736                         buf.append(_attrs[i].getValue());
737                         buf.append("\"");
738                     }
739                 }
740             }
741 
742             if (_list != null)
743             {
744                 if (tag)
745                     buf.append(">");
746                 for (int i = 0; i < _list.size(); i++)
747                 {
748                     Object o = _list.get(i);
749                     if (o == null)
750                         continue;
751                     if (o instanceof Node)
752                         ((Node) o).toString(buf, tag);
753                     else
754                         buf.append(o.toString());
755                 }
756                 if (tag)
757                 {
758                     buf.append("</");
759                     buf.append(_tag);
760                     buf.append(">");
761                 }
762             }
763             else if (tag)
764                 buf.append("/>");
765         }
766 
767         /* ------------------------------------------------------------ */
768         /**
769          * Iterator over named child nodes.
770          *
771          * @param tag The tag of the nodes.
772          * @return Iterator over all child nodes with the specified tag.
773          */
774         public Iterator<Node> iterator(final String tag)
775         {
776             return new Iterator<Node>()
777             {
778                 int c = 0;
779                 Node _node;
780 
781                 /* -------------------------------------------------- */
782                 public boolean hasNext()
783                 {
784                     if (_node != null)
785                         return true;
786                     while (_list != null && c < _list.size())
787                     {
788                         Object o = _list.get(c);
789                         if (o instanceof Node)
790                         {
791                             Node n = (Node) o;
792                             if (tag.equals(n._tag))
793                             {
794                                 _node = n;
795                                 return true;
796                             }
797                         }
798                         c++;
799                     }
800                     return false;
801                 }
802 
803                 /* -------------------------------------------------- */
804                 public Node next()
805                 {
806                     try
807                     {
808                         if (hasNext())
809                             return _node;
810                         throw new NoSuchElementException();
811                     }
812                     finally
813                     {
814                         _node = null;
815                         c++;
816                     }
817                 }
818 
819                 /* -------------------------------------------------- */
820                 public void remove()
821                 {
822                     throw new UnsupportedOperationException("Not supported");
823                 }
824             };
825         }
826     }
827 }