/**
* Copyright 2005-2014 Restlet
*
* The contents of this file are subject to the terms of one of the following
* open source licenses: Apache 2.0 or or EPL 1.0 (the "Licenses"). You can
* select the license that you prefer but you may not use this file except in
* compliance with one of these Licenses.
*
* You can obtain a copy of the Apache 2.0 license at
* http://www.opensource.org/licenses/apache-2.0
*
* You can obtain a copy of the EPL 1.0 license at
* http://www.opensource.org/licenses/eclipse-1.0
*
* See the Licenses for the specific language governing permissions and
* limitations under the Licenses.
*
* Alternatively, you can obtain a royalty free commercial license with less
* limitations, transferable or non-transferable, directly at
* http://restlet.com/products/restlet-framework
*
* Restlet is a registered trademark of Restlet S.A.S.
*/
package org.restlet.ext.xml;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathFactory;
import org.restlet.Context;
import org.restlet.data.MediaType;
import org.restlet.representation.Representation;
import org.restlet.representation.WriterRepresentation;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* Representation based on an XML document. It knows how to evaluate XPath
* expressions and how to manage a namespace context. This class also offers
* convenient methods to validate the document against a specified XML scheme.<br>
* <br>
* SECURITY WARNING: Using XML parsers configured to not prevent nor limit
* document type definition (DTD) entity resolution can expose the parser to an
* XML Entity Expansion injection attack.
*
* @see <a
* href="https://github.com/restlet/restlet-framework-java/wiki/XEE-security-enhancements">XML
* Entity Expansion injection attack</a>
* @author Jerome Louvel
*/
public abstract class XmlRepresentation extends WriterRepresentation
// [ifndef android]
implements javax.xml.namespace.NamespaceContext
// [enddef]
{
/**
* True for expanding entity references when parsing XML representations;
* default value provided by system property
* "org.restlet.ext.xml.expandingEntityRefs", false by default.
*/
public static boolean XML_EXPANDING_ENTITY_REFS = Boolean
.getBoolean("org.restlet.ext.xml.expandingEntityRefs");
/**
* True for validating DTD documents when parsing XML representations;
* default value provided by system property
* "org.restlet.ext.xml.validatingDtd", false by default.
*/
public static boolean XML_VALIDATING_DTD = Boolean
.getBoolean("org.restlet.ext.xml.validatingDtd");
// [ifdef android] method
/**
* Appends the text content of a given node and its descendants to the given
* buffer.
*
* @param node
* The node.
* @param sb
* The buffer.
*/
private static void appendTextContent(Node node, StringBuilder sb) {
switch (node.getNodeType()) {
case Node.TEXT_NODE:
sb.append(node.getNodeValue());
break;
case Node.CDATA_SECTION_NODE:
sb.append(node.getNodeValue());
break;
case Node.COMMENT_NODE:
sb.append(node.getNodeValue());
break;
case Node.PROCESSING_INSTRUCTION_NODE:
sb.append(node.getNodeValue());
break;
case Node.ENTITY_REFERENCE_NODE:
if (node.getNodeName().startsWith("#")) {
int ch = Integer.parseInt(node.getNodeName().substring(1));
sb.append((char) ch);
}
break;
case Node.ELEMENT_NODE:
for (int i = 0; i < node.getChildNodes().getLength(); i++) {
appendTextContent(node.getChildNodes().item(i), sb);
}
break;
case Node.ATTRIBUTE_NODE:
for (int i = 0; i < node.getChildNodes().getLength(); i++) {
appendTextContent(node.getChildNodes().item(i), sb);
}
break;
case Node.ENTITY_NODE:
for (int i = 0; i < node.getChildNodes().getLength(); i++) {
appendTextContent(node.getChildNodes().item(i), sb);
}
break;
case Node.DOCUMENT_FRAGMENT_NODE:
for (int i = 0; i < node.getChildNodes().getLength(); i++) {
appendTextContent(node.getChildNodes().item(i), sb);
}
break;
default:
break;
}
}
// [ifndef android] method
/**
* Returns a SAX source.
*
* @param xmlRepresentation
* The XML representation to wrap.
* @return A SAX source.
* @throws IOException
*/
public static javax.xml.transform.sax.SAXSource getSaxSource(
Representation xmlRepresentation) throws IOException {
javax.xml.transform.sax.SAXSource result = null;
if (xmlRepresentation != null) {
result = new javax.xml.transform.sax.SAXSource(new InputSource(
xmlRepresentation.getStream()));
if (xmlRepresentation.getLocationRef() != null) {
result.setSystemId(xmlRepresentation.getLocationRef()
.getTargetRef().toString());
}
}
return result;
}
// [ifndef android] method
/**
* Returns the wrapped schema.
*
* @return The wrapped schema.
* @throws IOException
*/
private static javax.xml.validation.Schema getSchema(
Representation schemaRepresentation) throws Exception {
javax.xml.validation.Schema result = null;
if (schemaRepresentation != null) {
final javax.xml.transform.stream.StreamSource streamSource = new javax.xml.transform.stream.StreamSource(
schemaRepresentation.getStream());
result = javax.xml.validation.SchemaFactory.newInstance(
getSchemaLanguageUri(schemaRepresentation)).newSchema(
streamSource);
}
return result;
}
/**
* Returns the schema URI for the current schema media type.
*
* @return The schema URI.
*/
private static String getSchemaLanguageUri(
Representation schemaRepresentation) {
String result = null;
if (schemaRepresentation != null) {
if (MediaType.APPLICATION_W3C_SCHEMA.equals(schemaRepresentation
.getMediaType())) {
result = XMLConstants.W3C_XML_SCHEMA_NS_URI;
} else if (MediaType.APPLICATION_RELAXNG_COMPACT
.equals(schemaRepresentation.getMediaType())) {
result = XMLConstants.RELAXNG_NS_URI;
} else if (MediaType.APPLICATION_RELAXNG_XML
.equals(schemaRepresentation.getMediaType())) {
result = XMLConstants.RELAXNG_NS_URI;
}
}
return result;
}
// [ifdef android] method
/**
* Returns the text content of a given node and its descendants.
*
* @param node
* The node.
* @return The text content of a given node.
*/
public static String getTextContent(Node node) {
StringBuilder sb = new StringBuilder();
appendTextContent(node, sb);
return sb.toString();
}
/**
* Specifies that the parser will convert CDATA nodes to text nodes and
* append it to the adjacent (if any) text node. By default the value of
* this is set to false.
*/
private volatile boolean coalescing;
/**
* A SAX {@link EntityResolver} to use when resolving external entity
* references while parsing this type of XML representations.
*
* @see DocumentBuilder#setEntityResolver(EntityResolver)
*/
private volatile EntityResolver entityResolver;
/**
* A SAX {@link ErrorHandler} to use for signaling SAX exceptions while
* parsing this type of XML representations.
*
* @see DocumentBuilder#setErrorHandler(ErrorHandler)
*/
private volatile ErrorHandler errorHandler;
/**
* Specifies that the parser will expand entity reference nodes. By default
* the value of this is set to true.
*/
private volatile boolean expandingEntityRefs;
/**
* Indicates if the parser will ignore comments. By default the value of
* this is set to false.
*/
private volatile boolean ignoringComments;
/**
* Indicates if the parser will ignore extra white spaces in element
* content. By default the value of this is set to false.
*/
private volatile boolean ignoringExtraWhitespaces;
/** Indicates if processing is namespace aware. */
private volatile boolean namespaceAware;
/** Internal map of namespaces. */
private volatile Map<String, String> namespaces;
// [ifndef android] member
/**
* A (compiled) {@link javax.xml.validation.Schema} to use when validating
* this type of XML representations.
*
* @see DocumentBuilderFactory#setSchema(javax.xml.validation.Schema)
*/
private volatile javax.xml.validation.Schema schema;
/**
* Indicates the desire for validating this type of XML representations
* against a DTD. Note that for XML schema or Relax NG validation, use the
* "schema" property instead.
*
* @see DocumentBuilderFactory#setValidating(boolean)
*/
private volatile boolean validatingDtd;
/**
* Indicates the desire for processing <em>XInclude</em> if found in this
* type of XML representations. By default the value of this is set to
* false.
*
* @see DocumentBuilderFactory#setXIncludeAware(boolean)
*/
private volatile boolean xIncludeAware;
/**
* Constructor.
*
* @param mediaType
* The representation's mediaType.
*/
public XmlRepresentation(MediaType mediaType) {
this(mediaType, UNKNOWN_SIZE);
}
/**
* Constructor.
*
* @param mediaType
* The representation's mediaType.
* @param expectedSize
* The expected input stream size.
*/
public XmlRepresentation(MediaType mediaType, long expectedSize) {
super(mediaType, expectedSize);
this.coalescing = false;
this.entityResolver = null;
this.errorHandler = null;
this.expandingEntityRefs = XML_EXPANDING_ENTITY_REFS;
this.ignoringComments = false;
this.ignoringExtraWhitespaces = false;
this.namespaceAware = false;
this.namespaces = null;
this.validatingDtd = XML_VALIDATING_DTD;
this.xIncludeAware = false;
// [ifndef android] line
this.schema = null;
}
// [ifndef android] method
/**
* Evaluates an XPath expression as a boolean. If the evaluation fails, null
* will be returned.
*
* @return The evaluation result.
*/
public Boolean getBoolean(String expression) {
return (Boolean) internalEval(expression,
javax.xml.xpath.XPathConstants.BOOLEAN);
}
/**
* Returns the XML representation as a DOM document.
*
* @return The DOM document.
*/
protected Document getDocument() throws Exception {
return getDocumentBuilder().parse(getInputSource());
}
/**
* Returns a document builder properly configured.
*
* @return A document builder properly configured.
*/
protected DocumentBuilder getDocumentBuilder() throws IOException {
DocumentBuilder result = null;
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(isNamespaceAware());
dbf.setValidating(isValidatingDtd());
dbf.setCoalescing(isCoalescing());
dbf.setExpandEntityReferences(isExpandingEntityRefs());
dbf.setIgnoringComments(isIgnoringComments());
dbf.setIgnoringElementContentWhitespace(isIgnoringExtraWhitespaces());
try {
dbf.setXIncludeAware(isXIncludeAware());
} catch (UnsupportedOperationException uoe) {
Context.getCurrentLogger().log(Level.FINE,
"The JAXP parser doesn't support XInclude.", uoe);
}
// [ifndef android]
javax.xml.validation.Schema xsd = getSchema();
if (xsd != null) {
dbf.setSchema(xsd);
}
// [enddef]
result = dbf.newDocumentBuilder();
result.setEntityResolver(getEntityResolver());
result.setErrorHandler(getErrorHandler());
} catch (ParserConfigurationException pce) {
throw new IOException("Couldn't create the empty document: "
+ pce.getMessage());
}
return result;
}
// [ifndef android] method
/**
* Returns a DOM source.
*
* @return A DOM source.
* @throws IOException
*/
public javax.xml.transform.dom.DOMSource getDomSource() throws IOException {
javax.xml.transform.dom.DOMSource result = null;
Node document = null;
try {
document = getDocumentBuilder().parse(getInputSource());
} catch (SAXException se) {
throw new IOException("Couldn't read the XML representation. "
+ se.getMessage());
}
if (document != null) {
result = new javax.xml.transform.dom.DOMSource(document);
if (getLocationRef() != null) {
result.setSystemId(getLocationRef().getTargetRef().toString());
}
}
return result;
}
/**
* Return the possibly null current SAX {@link EntityResolver}.
*
* @return The possibly null current SAX {@link EntityResolver}.
*/
public EntityResolver getEntityResolver() {
return entityResolver;
}
/**
* Return the possibly null current SAX {@link ErrorHandler}.
*
* @return The possibly null current SAX {@link ErrorHandler}.
*/
public ErrorHandler getErrorHandler() {
return errorHandler;
}
/**
* Returns the XML representation as a SAX input source.
*
* @return The SAX input source.
*/
public abstract InputSource getInputSource() throws IOException;
/**
* Returns the map of namespaces. Namespace prefixes are keys and URI
* references are values.
*
* @return The map of namespaces.
*/
public Map<String, String> getNamespaces() {
if (this.namespaces == null) {
this.namespaces = new HashMap<String, String>();
}
return this.namespaces;
}
/**
* {@inheritDoc
* javax.xml.namespace.NamespaceContext#getNamespaceURI(java.lang.String}
*/
public String getNamespaceURI(String prefix) {
return (this.namespaces == null) ? null : this.namespaces.get(prefix);
}
// [ifndef android] method
/**
* Evaluates an XPath expression as a DOM Node. If the evaluation fails,
* null will be returned.
*
* @return The evaluation result.
*/
public Node getNode(String expression) {
return (Node) internalEval(expression,
javax.xml.xpath.XPathConstants.NODE);
}
// [ifndef android] method
/**
* Evaluates an XPath expression as a DOM NodeList. If the evaluation fails,
* null will be returned.
*
* @return The evaluation result.
*/
public NodeList getNodes(String expression) {
final org.w3c.dom.NodeList nodes = (org.w3c.dom.NodeList) internalEval(
expression, javax.xml.xpath.XPathConstants.NODESET);
return (nodes == null) ? null : new NodeList(nodes);
}
// [ifndef android] method
/**
* Evaluates an XPath expression as a number. If the evaluation fails, null
* will be returned.
*
* @return The evaluation result.
*/
public Double getNumber(String expression) {
return (Double) internalEval(expression,
javax.xml.xpath.XPathConstants.NUMBER);
}
/**
* {@inheritDoc
* javax.xml.namespace.NamespaceContext#getPrefix(java.lang.String}
*/
public String getPrefix(String namespaceURI) {
String result = null;
boolean found = false;
for (Iterator<String> iterator = getNamespaces().keySet().iterator(); iterator
.hasNext() && !found;) {
String key = iterator.next();
if (getNamespaces().get(key).equals(namespaceURI)) {
found = true;
result = key;
}
}
return result;
}
/**
* {@inheritDoc
* javax.xml.namespace.NamespaceContext#getPrefixes(java.lang.String}
*/
public Iterator<String> getPrefixes(String namespaceURI) {
final List<String> result = new ArrayList<String>();
for (Iterator<String> iterator = getNamespaces().keySet().iterator(); iterator
.hasNext();) {
String key = iterator.next();
if (getNamespaces().get(key).equals(namespaceURI)) {
result.add(key);
}
}
return Collections.unmodifiableList(result).iterator();
}
// [ifndef android] method
/**
* Returns a SAX source.
*
* @return A SAX source.
* @throws IOException
*/
public javax.xml.transform.sax.SAXSource getSaxSource() throws IOException {
return getSaxSource(this);
}
// [ifndef android] method
/**
* Return the possibly null {@link javax.xml.validation.Schema} to use for
* this type of XML representations.
*
* @return the {@link javax.xml.validation.Schema} object of this type of
* XML representations.
*/
public javax.xml.validation.Schema getSchema() {
return schema;
}
// [ifndef android] method
/**
* Returns a stream of XML markup.
*
* @return A stream of XML markup.
* @throws IOException
*/
public javax.xml.transform.stream.StreamSource getStreamSource()
throws IOException {
final javax.xml.transform.stream.StreamSource result = new javax.xml.transform.stream.StreamSource(
getStream());
if (getLocationRef() != null) {
result.setSystemId(getLocationRef().getTargetRef().toString());
}
return result;
}
// [ifndef android] method
/**
* Evaluates an XPath expression as a string.
*
* @return The evaluation result.
*/
public String getText(String expression) {
return (String) internalEval(expression,
javax.xml.xpath.XPathConstants.STRING);
}
// [ifndef android] method
/**
* Evaluates an XPath expression and returns the result as in the given
* return type.
*
* @param returnType
* The qualified name of the return type.
* @return The evaluation result.
*/
private Object internalEval(String expression,
javax.xml.namespace.QName returnType) {
try {
Object result = null;
XPath xpath = XPathFactory.newInstance().newXPath();
xpath.setNamespaceContext(this);
Document xmlDocument = getDocument();
if (xmlDocument != null) {
result = xpath.evaluate(expression, xmlDocument, returnType);
} else {
throw new Exception(
"Unable to obtain a DOM document for the XML representation. "
+ "XPath evaluation cancelled.");
}
return result;
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Indicates if the parser should be coalescing text. If true the parser
* will convert CDATA nodes to text nodes and append it to the adjacent (if
* any) text node. By default the value of this is set to false.
*
* @return True if parser should be coalescing text.
*/
public boolean isCoalescing() {
return coalescing;
}
/**
* Indicates if the parser will expand entity reference nodes. By default
* the value of this is set to true.
*
* @return True if the parser will expand entity reference nodes.
*/
public boolean isExpandingEntityRefs() {
return expandingEntityRefs;
}
/**
* Indicates if the parser will ignore comments. By default the value of
* this is set to false.
*
* @return True if the parser will ignore comments.
*/
public boolean isIgnoringComments() {
return ignoringComments;
}
/**
* Indicates if the parser will ignore extra white spaces in element
* content. Note that the {@link #isValidatingDtd()} must be true when this
* property is 'true' as validation is needed for it to work. By default the
* value of this is set to false.
*
* @return True if the parser will ignore extra white spaces.
*/
public boolean isIgnoringExtraWhitespaces() {
return ignoringExtraWhitespaces;
}
/**
* Indicates if processing is namespace aware.
*
* @return True if processing is namespace aware.
*/
public boolean isNamespaceAware() {
return this.namespaceAware;
}
/**
* Indicates the desire for validating this type of XML representations
* against an XML schema if one is referenced within the contents.
*
* @return True if the schema-based validation is enabled.
*/
public boolean isValidatingDtd() {
return validatingDtd;
}
/**
* Indicates the desire for processing <em>XInclude</em> if found in this
* type of XML representations. By default the value of this is set to
* false.
*
* @return The current value of the xIncludeAware flag.
*/
public boolean isXIncludeAware() {
return xIncludeAware;
}
/**
* Releases the namespaces map.
*/
@Override
public void release() {
if (this.namespaces != null) {
this.namespaces.clear();
this.namespaces = null;
}
super.release();
}
/**
* Indicates if the parser should be coalescing text. If true the parser
* will convert CDATA nodes to text nodes and append it to the adjacent (if
* any) text node. By default the value of this is set to false.
*
* @param coalescing
* True if parser should be coalescing text.
*/
public void setCoalescing(boolean coalescing) {
this.coalescing = coalescing;
}
/**
* Set the {@link EntityResolver} to use when resolving external entity
* references encountered in this type of XML representations.
*
* @param entityResolver
* the {@link EntityResolver} to set.
*/
public void setEntityResolver(EntityResolver entityResolver) {
this.entityResolver = entityResolver;
}
/**
* Set the {@link ErrorHandler} to use when signaling SAX event exceptions.
*
* @param errorHandler
* the {@link ErrorHandler} to set.
*/
public void setErrorHandler(ErrorHandler errorHandler) {
this.errorHandler = errorHandler;
}
/**
* Indicates if the parser will expand entity reference nodes. By default
* the value of this is set to true.
*
* @param expandEntityRefs
* True if the parser will expand entity reference nodes.
*/
public void setExpandingEntityRefs(boolean expandEntityRefs) {
this.expandingEntityRefs = expandEntityRefs;
}
/**
* Indicates if the parser will ignore comments. By default the value of
* this is set to false.
*
* @param ignoringComments
* True if the parser will ignore comments.
*/
public void setIgnoringComments(boolean ignoringComments) {
this.ignoringComments = ignoringComments;
}
/**
* Indicates if the parser will ignore extra white spaces in element
* content. Note that the {@link #setValidatingDtd(boolean)} will be invoked
* with 'true' if setting this property to 'true' as validation is needed
* for it to work.
*
* @param ignoringExtraWhitespaces
* True if the parser will ignore extra white spaces in element
* content.
*/
public void setIgnoringExtraWhitespaces(boolean ignoringExtraWhitespaces) {
if (this.ignoringExtraWhitespaces != ignoringExtraWhitespaces) {
if (ignoringExtraWhitespaces) {
setValidatingDtd(true);
}
this.ignoringExtraWhitespaces = ignoringExtraWhitespaces;
}
}
/**
* Indicates if processing is namespace aware.
*
* @param namespaceAware
* Indicates if processing is namespace aware.
*/
public void setNamespaceAware(boolean namespaceAware) {
this.namespaceAware = namespaceAware;
}
/**
* Sets the map of namespaces.
*
* @param namespaces
* The map of namespaces.
*/
public void setNamespaces(Map<String, String> namespaces) {
this.namespaces = namespaces;
}
// [ifndef android] method
/**
* Set a (compiled) {@link javax.xml.validation.Schema} to use when parsing
* and validating this type of XML representations.
*
* @param schema
* The (compiled) {@link javax.xml.validation.Schema} object to
* set.
*/
public void setSchema(javax.xml.validation.Schema schema) {
this.schema = schema;
}
// [ifndef android] method
/**
* Set a schema representation to be compiled and used when parsing and
* validating this type of XML representations.
*
* @param schemaRepresentation
* The schema representation to set.
*/
public void setSchema(Representation schemaRepresentation) {
try {
this.schema = getSchema(schemaRepresentation);
} catch (Exception e) {
Context.getCurrentLogger().log(Level.WARNING,
"Unable to compile the schema representation", e);
}
}
/**
* Indicates the desire for validating this type of XML representations
* against an XML schema if one is referenced within the contents.
*
* @param validating
* The new validation flag to set.
*/
public void setValidatingDtd(boolean validating) {
this.validatingDtd = validating;
}
/**
* Indicates the desire for processing <em>XInclude</em> if found in this
* type of XML representations. By default the value of this is set to
* false.
*
* @param includeAware
* The new value of the xIncludeAware flag.
*/
public void setXIncludeAware(boolean includeAware) {
xIncludeAware = includeAware;
}
// [ifndef android] method
/**
* Validates the XML representation against a given schema.
*
* @param schema
* The XML schema to use.
*/
public void validate(javax.xml.validation.Schema schema) throws Exception {
validate(schema, null);
}
// [ifndef android] method
/**
* Validates the XML representation against a given schema.
*
* @param schema
* The XML schema to use.
* @param result
* The Result object that receives (possibly augmented) XML.
*/
public void validate(javax.xml.validation.Schema schema,
javax.xml.transform.Result result) throws Exception {
schema.newValidator().validate(getSaxSource(), result);
}
// [ifndef android] method
/**
* Validates the XML representation against a given schema.
*
* @param schemaRepresentation
* The XML schema representation to use.
*/
public void validate(Representation schemaRepresentation) throws Exception {
validate(schemaRepresentation, null);
}
// [ifndef android] method
/**
* Validates the XML representation against a given schema.
*
* @param schemaRepresentation
* The XML schema representation to use.
* @param result
* The Result object that receives (possibly augmented) XML.
*/
public void validate(Representation schemaRepresentation,
javax.xml.transform.Result result) throws Exception {
validate(getSchema(schemaRepresentation), result);
}
}