All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jetty.xml.XmlParser Maven / Gradle / Ivy

The newest version!
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.xml;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Stack;
import java.util.StringTokenizer;
import javax.xml.catalog.Catalog;
import javax.xml.catalog.CatalogFeatures;
import javax.xml.catalog.CatalogManager;
import javax.xml.catalog.CatalogResolver;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.eclipse.jetty.util.LazyList;
import org.eclipse.jetty.util.thread.AutoLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

/**
 * XML Parser wrapper. This class wraps any standard JAXP1.1 parser with convenient error and
 * entity handlers and a mini dom-like document tree.
 * 

* By default, the parser is created as a validating parser only if xerces is present. This can be * configured by setting the "org.eclipse.jetty.xml.XmlParser.Validating" system property. */ public class XmlParser { private static final Logger LOG = LoggerFactory.getLogger(XmlParser.class); private final AutoLock _lock = new AutoLock(); private SAXParser _parser; private Map _observerMap; private Stack _observers = new Stack(); private String _xpath; private Object _xpaths; private String _dtd; private List _entityResolvers = new ArrayList<>(); /** * Construct XmlParser */ public XmlParser() { this(getValidatingDefault()); } /** * Construct XmlParser * * @param validating true to enable validation, false to disable * @see SAXParserFactory#setValidating(boolean) */ public XmlParser(boolean validating) { setValidating(validating); URL url = XmlParser.class.getResource("catalog-org.w3.xml"); if (url == null) throw new IllegalStateException("Catalog not found: catalog-org.w3.xml"); addCatalog(URI.create(url.toExternalForm())); } private static boolean getValidatingDefault() { SAXParserFactory factory = SAXParserFactory.newInstance(); boolean validatingDefault = factory.getClass().toString().contains("org.apache.xerces."); String validatingProp = System.getProperty("org.eclipse.jetty.xml.XmlParser.Validating", validatingDefault ? "true" : "false"); return Boolean.parseBoolean(validatingProp); } AutoLock lock() { return _lock.lock(); } protected SAXParserFactory newSAXParserFactory() { return SAXParserFactory.newInstance(); } public void setValidating(boolean validating) { try { SAXParserFactory factory = newSAXParserFactory(); factory.setValidating(validating); _parser = factory.newSAXParser(); try { if (validating) _parser.getXMLReader().setFeature("http://apache.org/xml/features/validation/schema", validating); } catch (Exception e) { if (validating) LOG.warn("Schema validation may not be supported: ", e); else LOG.trace("IGNORED", e); } _parser.getXMLReader().setFeature("http://xml.org/sax/features/validation", validating); _parser.getXMLReader().setFeature("http://xml.org/sax/features/namespaces", true); _parser.getXMLReader().setFeature("http://xml.org/sax/features/namespace-prefixes", false); try { if (validating) _parser.getXMLReader().setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", validating); } catch (Exception e) { LOG.warn(e.getMessage()); } } catch (Exception e) { LOG.warn("Unable to set validating on XML Parser", e); throw new Error(e.toString()); } } public boolean isValidating() { return _parser.isValidating(); } public SAXParser getSAXParser() { return _parser; } /** * Load the specified URI as a catalog for entity mapping purposes. * *

* This is a temporary Catalog implementation, and should be removed once * all of our usages of {@code servlet-api-.jar} have their own * {@code catalog.xml} files. *

* * @param catalogXml the URI pointing to the XML catalog * @param baseClassLocation the base class to use for finding relative resources defined in the Catalog XML. * This is resolved to the Class location with package location and is used as the XML Catalog Base URI. */ public void addCatalog(URI catalogXml, Class baseClassLocation) throws IOException { BaseClassCatalog catalog = BaseClassCatalog.load(catalogXml, baseClassLocation); _entityResolvers.add(catalog); } /** * Load the specified URI as a catalog for entity mapping purposes. * * @param catalogXml the uri to the catalog */ public void addCatalog(URI catalogXml) { CatalogFeatures f = CatalogFeatures.builder().with(CatalogFeatures.Feature.RESOLVE, "continue").build(); Catalog catalog = CatalogManager.catalog(f, catalogXml); CatalogResolver catalogResolver = CatalogManager.catalogResolver(catalog); _entityResolvers.add(catalogResolver); } /** * @return Returns the xpath. */ public String getXpath() { return _xpath; } /** * Set an XPath A very simple subset of xpath is supported to select a partial tree. Currently * only path like "/node1/nodeA | /node1/nodeB" are supported. * * @param xpath The xpath to set. */ public void setXpath(String xpath) { _xpath = xpath; StringTokenizer tok = new StringTokenizer(xpath, "| "); while (tok.hasMoreTokens()) { _xpaths = LazyList.add(_xpaths, tok.nextToken()); } } public String getDTD() { return _dtd; } /** * Add a ContentHandler. Add an additional _content handler that is triggered on a tag name. SAX * events are passed to the ContentHandler provided from a matching start element to the * corresponding end element. Only a single _content handler can be registered against each tag. * * @param trigger Tag local or q name. * @param observer SAX ContentHandler */ public void addContentHandler(String trigger, ContentHandler observer) { try (AutoLock l = _lock.lock()) { if (_observerMap == null) _observerMap = new HashMap<>(); _observerMap.put(trigger, observer); } } public Node parse(InputSource source) throws IOException, SAXException { try (AutoLock l = _lock.lock()) { _dtd = null; Handler handler = new Handler(); XMLReader reader = _parser.getXMLReader(); reader.setContentHandler(handler); reader.setErrorHandler(handler); reader.setEntityResolver(handler); if (LOG.isDebugEnabled()) LOG.debug("parsing: sid={},pid={}", source.getSystemId(), source.getPublicId()); _parser.parse(source, handler); if (handler._error != null) throw handler._error; Node doc = (Node)handler._top.get(0); handler.clear(); return doc; } } /** * Parse String URL. * * @param url the url to the xml to parse * @return the root node of the xml * @throws IOException if unable to load the xml * @throws SAXException if unable to parse the xml */ public Node parse(String url) throws IOException, SAXException { if (LOG.isDebugEnabled()) LOG.debug("parse: {}", url); return parse(new InputSource(url)); } /** * Parse File. * * @param file the file to the xml to parse * @return the root node of the xml * @throws IOException if unable to load the xml * @throws SAXException if unable to parse the xml */ public Node parse(File file) throws IOException, SAXException { if (LOG.isDebugEnabled()) LOG.debug("parse: {}", file); return parse(new InputSource(file.toURI().toURL().toString())); } /** * Parse InputStream. * * @param in the input stream of the xml to parse * @return the root node of the xml * @throws IOException if unable to load the xml * @throws SAXException if unable to parse the xml */ public Node parse(InputStream in) throws IOException, SAXException { return parse(new InputSource(in)); } InputSource resolveEntity(String pid, String sid) { if (LOG.isDebugEnabled()) LOG.debug("resolveEntity({},{})", pid, sid); for (EntityResolver entityResolver : _entityResolvers) { try { InputSource src = entityResolver.resolveEntity(pid, sid); if (src != null) return src; } catch (IOException | SAXException e) { LOG.trace("IGNORE EntityResolver exception for (pid=%s, sid=%s)".formatted(pid, sid), e); } } if (LOG.isDebugEnabled()) LOG.debug("Entity not found for PID:{} / SID:{}", pid, sid); return null; } private class NoopHandler extends DefaultHandler { Handler _next; int _depth; NoopHandler(Handler next) { this._next = next; } @Override public void startElement(String uri, String localName, String qName, Attributes attrs) throws SAXException { _depth++; } @Override public void endElement(String uri, String localName, String qName) throws SAXException { if (_depth == 0) _parser.getXMLReader().setContentHandler(_next); else _depth--; } } private class Handler extends DefaultHandler { Node _top = new Node(null, null, null); SAXParseException _error; private Node _context = _top; private NoopHandler _noop; Handler() { _noop = new NoopHandler(this); } void clear() { _top = null; _error = null; _context = null; } @Override public void startElement(String uri, String localName, String qName, Attributes attrs) throws SAXException { String name = null; if (_parser.isNamespaceAware()) name = localName; if (name == null || "".equals(name)) name = qName; Node node = new Node(_context, name, attrs); // check if the node matches any xpaths set? if (_xpaths != null) { String path = node.getPath(); boolean match = false; for (int i = LazyList.size(_xpaths); !match && i-- > 0; ) { String xpath = (String)LazyList.get(_xpaths, i); match = path.equals(xpath) || xpath.startsWith(path) && xpath.length() > path.length() && xpath.charAt(path.length()) == '/'; } if (match) { _context.add(node); _context = node; } else { _parser.getXMLReader().setContentHandler(_noop); } } else { _context.add(node); _context = node; } ContentHandler observer = null; if (_observerMap != null) observer = (ContentHandler)_observerMap.get(name); _observers.push(observer); for (int i = 0; i < _observers.size(); i++) { if (_observers.get(i) != null) ((ContentHandler)_observers.get(i)).startElement(uri, localName, qName, attrs); } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { _context = _context._parent; for (int i = 0; i < _observers.size(); i++) { if (_observers.get(i) != null) ((ContentHandler)_observers.get(i)).endElement(uri, localName, qName); } _observers.pop(); } @Override public void ignorableWhitespace(char[] buf, int offset, int len) throws SAXException { for (int i = 0; i < _observers.size(); i++) { if (_observers.get(i) != null) ((ContentHandler)_observers.get(i)).ignorableWhitespace(buf, offset, len); } } @Override public void characters(char[] buf, int offset, int len) throws SAXException { _context.add(new String(buf, offset, len)); for (int i = 0; i < _observers.size(); i++) { if (_observers.get(i) != null) ((ContentHandler)_observers.get(i)).characters(buf, offset, len); } } @Override public void warning(SAXParseException ex) { if (LOG.isDebugEnabled()) LOG.warn("SAX Parse Issue", ex); else LOG.warn("SAX Parse Issue @{} : {}", getLocationString(ex), ex.toString()); } @Override public void error(SAXParseException ex) throws SAXException { // Save error and continue to report other errors if (_error == null) _error = ex; if (LOG.isDebugEnabled()) LOG.error("SAX Parse Issue", ex); else LOG.error("SAX Parse Issue @{} : {}", getLocationString(ex), ex.toString()); } @Override public void fatalError(SAXParseException ex) throws SAXException { _error = ex; if (LOG.isDebugEnabled()) LOG.error("Fatal AX Parse Issue", ex); else LOG.error("Fatal SAX Parse Issue @{} : {}", getLocationString(ex), ex.toString()); throw ex; } private String getLocationString(SAXParseException ex) { return ex.getSystemId() + " line:" + ex.getLineNumber() + " col:" + ex.getColumnNumber(); } @Override public InputSource resolveEntity(String pid, String sid) { return XmlParser.this.resolveEntity(pid, sid); } } /** * XML Attribute. */ public static class Attribute { private String _name; private String _value; Attribute(String n, String v) { _name = n; _value = v; } public String getName() { return _name; } public String getValue() { return _value; } } /** * XML Node. Represents an XML element with optional attributes and ordered content. */ public static class Node extends AbstractList { Node _parent; private ArrayList _list; private String _tag; private Attribute[] _attrs; private boolean _lastString = false; private String _path; Node(Node parent, String tag, Attributes attrs) { _parent = parent; _tag = tag; if (attrs != null) { _attrs = new Attribute[attrs.getLength()]; for (int i = 0; i < attrs.getLength(); i++) { String name = attrs.getLocalName(i); if (name == null || name.isEmpty()) name = attrs.getQName(i); _attrs[i] = new Attribute(name, attrs.getValue(i)); } } } public Node getParent() { return _parent; } public String getTag() { return _tag; } public String getPath() { if (_path == null) { if (getParent() != null && getParent().getTag() != null) _path = getParent().getPath() + "/" + _tag; else _path = "/" + _tag; } return _path; } /** * Get an array of element attributes. * * @return the attributes */ public Attribute[] getAttributes() { return _attrs; } /** * Get an element attribute. * * @param name the name of the attribute * @return attribute or null. */ public String getAttribute(String name) { return getAttribute(name, null); } /** * Get an element attribute. * * @param name the name of the element * @param dft the default value * @return attribute or null. */ public String getAttribute(String name, String dft) { if (_attrs == null || name == null) return dft; for (int i = 0; i < _attrs.length; i++) { if (name.equals(_attrs[i].getName())) return _attrs[i].getValue(); } return dft; } /** * Get the number of children nodes. */ @Override public int size() { if (_list != null) return _list.size(); return 0; } /** * Get the ith child node or content. * * @return Node or String. */ @Override public Object get(int i) { if (_list != null) return _list.get(i); return null; } /** * Get the first child node with the tag. * * @param tag the name of the tag * @return Node or null. */ public Node get(String tag) { if (_list != null) { for (int i = 0; i < _list.size(); i++) { Object o = _list.get(i); if (o instanceof Node) { Node n = (Node)o; if (tag.equals(n._tag)) return n; } } } return null; } @Override public void add(int i, Object o) { if (_list == null) _list = new ArrayList(); if (o instanceof String) { if (_lastString) { int last = _list.size() - 1; _list.set(last, (String)_list.get(last) + o); } else _list.add(i, o); _lastString = true; } else { _lastString = false; _list.add(i, o); } } @Override public void clear() { if (_list != null) _list.clear(); _list = null; } /** * Get a tag as a string. * * @param tag The tag to get * @param tags IF true, tags are included in the value. * @param trim If true, trim the value. * @return results of get(tag).toString(tags). */ public String getString(String tag, boolean tags, boolean trim) { Node node = get(tag); if (node == null) return null; String s = node.toString(tags); if (s != null && trim) s = s.trim(); return s; } @Override public String toString() { return toString(true); } /** * Convert to a string. * * @param tag If false, only _content is shown. * @return the string value */ public String toString(boolean tag) { StringBuilder buf = new StringBuilder(); toString(buf, tag); return buf.toString(); } /** * Convert to a string. * * @param tag If false, only _content is shown. * @param trim true to trim the content * @return the trimmed content */ public String toString(boolean tag, boolean trim) { String s = toString(tag); if (s != null && trim) s = s.trim(); return s; } private void toString(StringBuilder buf, boolean tag) { if (tag) { buf.append("<"); buf.append(_tag); if (_attrs != null) { for (int i = 0; i < _attrs.length; i++) { buf.append(' '); buf.append(_attrs[i].getName()); buf.append("=\""); buf.append(_attrs[i].getValue()); buf.append("\""); } } } if (_list != null) { if (tag) buf.append(">"); for (int i = 0; i < _list.size(); i++) { Object o = _list.get(i); if (o == null) continue; if (o instanceof Node) ((Node)o).toString(buf, tag); else buf.append(o.toString()); } if (tag) { buf.append(""); } } else if (tag) buf.append("/>"); } /** * Iterator over named child nodes. * * @param tag The tag of the nodes. * @return Iterator over all child nodes with the specified tag. */ public Iterator iterator(final String tag) { return new Iterator() { int c = 0; Node _node; @Override public boolean hasNext() { if (_node != null) return true; while (_list != null && c < _list.size()) { Object o = _list.get(c); if (o instanceof Node) { Node n = (Node)o; if (tag.equals(n._tag)) { _node = n; return true; } } c++; } return false; } @Override public Node next() { try { if (hasNext()) return _node; throw new NoSuchElementException(); } finally { _node = null; c++; } } @Override public void remove() { throw new UnsupportedOperationException("Not supported"); } }; } } }