org.eclipse.jetty.xml.XmlParser Maven / Gradle / Ivy
//
// ========================================================================
// 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