
net.sf.saxon.event.StreamWriterToReceiver Maven / Gradle / Ivy
Show all versions of Saxon-HE Show documentation
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2015 Saxonica Limited.
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
package net.sf.saxon.event;
import net.sf.saxon.expr.parser.ExplicitLocation;
import net.sf.saxon.lib.StandardURIChecker;
import net.sf.saxon.om.*;
import net.sf.saxon.serialize.charcode.UTF16CharacterSet;
import net.sf.saxon.trans.Err;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.type.BuiltInAtomicType;
import net.sf.saxon.type.Untyped;
import net.sf.saxon.z.IntPredicate;
import javax.xml.namespace.NamespaceContext;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import java.util.*;
/**
* This class implements the XmlStreamWriter interface, translating the events into Saxon
* Receiver events. The Receiver can be anything: a serializer, a schema validator, a tree builder.
*
* This class does not itself perform "namespace repairing" as defined in the interface Javadoc
* (also referred to as "prefix defaulting" in the StaX JSR specification). In normal use, however,
* the events emitted by this class are piped into a {@link NamespaceReducer} which performs a function
* very similar to namespace repairing; specifically, it ensures that when elements and attribute are
* generated with a given namespace URI and local name, then namespace declarations are generated
* automatically without any explicit need to call the {@link #writeNamespace(String, String)} method.
*
* The class will check all names, URIs, and character content for conformance against XML well-formedness
* rules unless the checkValues
option is set to false.
*
* The implementation of this class is influenced not only by the Javadoc documentation of the
* XMLStreamWriter
interface (which is woefully inadequate), but also by the helpful
* but unofficial interpretation of the spec to be found at
* http://veithen.github.io/2009/11/01/understanding-stax.html
*
* @since 9.3. Rewritten May 2015 to fix bug 2357. Further modified in 9.7.0.2 in light of the discussion
* of bug 2398, and the interpretation of the spec cited above.
*/
public class StreamWriterToReceiver implements XMLStreamWriter {
private static class Triple {
public String prefix;
public String uri;
public String local;
public String value;
}
private static class StartTag {
public Triple elementName;
public List attributes;
public List namespaces;
public StartTag() {
elementName = new Triple();
attributes = new ArrayList();
namespaces = new ArrayList();
}
}
private StartTag pendingTag = null;
/**
* The receiver to which events will be passed
*/
private Receiver receiver;
/**
* The Saxon NamePool
*/
private NamePool namePool;
/**
* The Checker used for testing valid characters
*/
private IntPredicate charChecker;
/**
* Flag to indicate whether names etc are to be checked for well-formedness
*/
private boolean isChecking = false;
/**
* The current depth of element nesting. -1 indicates outside startDocument/endDocument; non-negative
* values indicate the number of open start element tags
*/
private int depth = -1;
/**
* Flag indicating that an empty element has been requested.
*/
private boolean isEmptyElement;
/**
* inScopeNamespaces represents namespaces that have been declared in the XML stream
*/
private NamespaceReducer inScopeNamespaces;
/**
* setPrefixes represents the namespace bindings that have been set, at each level of the stack,
* using {@link #setPrefix}. There is one entry for each level of element nesting, and the entry is a list
* of NamespaceBinding objects, that is, prefix/uri pairs
*/
private Stack> setPrefixes = new Stack>();
/**
* rootNamespaceContext is the namespace context supplied at the start, is the final fallback
* for allocating a prefix to a URI
*/
private javax.xml.namespace.NamespaceContext rootNamespaceContext = null;
/**
* Constructor. Creates a StreamWriter as a front-end to a given Receiver.
*
* @param receiver the Receiver that is to receive the events generated
* by this StreamWriter.
*/
public StreamWriterToReceiver(Receiver receiver) {
// Events are passed through a NamespaceReducer which maintains the namespace context
// It also eliminates duplicate namespace declarations, and creates extra namespace declarations
// where needed to support prefix-uri mappings used on elements and attributes
PipelineConfiguration pipe = receiver.getPipelineConfiguration();
this.inScopeNamespaces = new NamespaceReducer(receiver);
this.receiver = inScopeNamespaces;
//this.receiver = new TracingFilter(this.receiver);
this.charChecker = pipe.getConfiguration().getValidCharacterChecker();
this.namePool = pipe.getConfiguration().getNamePool();
this.setPrefixes.push(new ArrayList());
}
/**
* Say whether prefixes are to be invented when none is specified by the user.
* This method has no effect, and is deprecated
*
* @param invent true if prefixes are to be invented.
* @deprecated since 9.7.0.2. In normal use this class feeds its output into a {@link NamespaceReducer}
* which performs the equivalent of "namespace repairing" or "prefix invention": that is, it ensures
* that when elements or attributes are written in a specific namespace, there is no need for an
* explicit {@link #writeNamespace(String, String)} call to create the namespace declaration.
*/
public void setInventPrefixes(boolean invent) {
// no action
}
/**
* Ask whether prefixes are to be invented when none is specified by the user.
* This method is deprecated.
*
* @return true if the next receiver in the pipeline is a {@link NamespaceReducer}; this is an approximation
* of the result in earlier releases.
* @deprecated since 9.7.0.2
*/
public boolean isInventPrefixes() {
return receiver instanceof NamespaceReducer;
}
/**
* Say whether names and values are to be checked for conformance with XML rules
*
* @param check true if names and values are to be checked. Default is false;
*/
public void setCheckValues(boolean check) {
this.isChecking = check;
}
/**
* Ask whether names and values are to be checked for conformance with XML rules
*
* @return true if names and values are to be checked. Default is false;
*/
public boolean isCheckValues() {
return this.isChecking;
}
public void flushStartTag() throws XMLStreamException {
if (depth == -1) {
writeStartDocument();
}
if (pendingTag != null) {
try {
completeTriple(pendingTag.elementName, false);
for (Triple t : pendingTag.attributes) {
completeTriple(t, true);
}
NodeName nc;
if (pendingTag.elementName.uri.isEmpty()) {
nc = new NoNamespaceName(pendingTag.elementName.local);
} else {
nc = new FingerprintedQName(pendingTag.elementName.prefix, pendingTag.elementName.uri, pendingTag.elementName.local);
}
receiver.startElement(nc, Untyped.getInstance(), ExplicitLocation.UNKNOWN_LOCATION, 0);
for (Triple t : pendingTag.namespaces) {
if (t.prefix == null) {
t.prefix = "";
}
if (t.uri == null) {
t.uri = "";
}
receiver.namespace(new NamespaceBinding(t.prefix, t.uri), 0);
}
for (Triple t : pendingTag.attributes) {
if (t.uri.isEmpty()) {
nc = new NoNamespaceName(t.local);
} else {
nc = new FingerprintedQName(t.prefix, t.uri, t.local);
}
receiver.attribute(nc, BuiltInAtomicType.UNTYPED_ATOMIC, t.value, ExplicitLocation.UNKNOWN_LOCATION, 0);
}
pendingTag = null;
receiver.startContent();
if (isEmptyElement) {
isEmptyElement = false;
depth--;
setPrefixes.pop();
receiver.endElement();
}
} catch (XPathException e) {
throw new XMLStreamException(e);
}
}
}
/**
* Fill in the unknown parts of a (prefix, uri, localname) triple by reference to the namespace
* context.
* For details see the table in the XMLStreamWriter
javadoc.
* @param t the (prefix, uri, localname) triple. Note that the prefix will be null if not supplied
* in the call, and will be "" if an empty string or null was supplied in the call; these
* cases are handled differently.
* @param isAttribute true if this is an attribute name rather than an element name
* @return a list of namespace bindings that need to be output (null if empty)
* @throws XMLStreamException
*/
private void completeTriple(Triple t, boolean isAttribute) throws XMLStreamException {
if (t.local == null) {
throw new XMLStreamException("Local name of " + (isAttribute ? "Attribute" : "Element") + " is missing");
}
if (isChecking && !isValidNCName(t.local)) {
throw new XMLStreamException("Local name of " + (isAttribute ? "Attribute" : "Element") +
Err.wrap(t.local) + " is invalid");
}
if (t.prefix == null) {
t.prefix = "";
}
if (t.uri == null) {
t.uri = "";
}
if (isChecking && !t.uri.isEmpty() && !isValidURI(t.uri)) {
throw new XMLStreamException("Namespace URI " + Err.wrap(t.local) + " is invalid");
}
if (t.prefix.isEmpty() && !t.uri.isEmpty()) {
t.prefix = getPrefixForUri(t.uri);
}
}
private String getDefaultNamespace() {
for (Triple t : pendingTag.namespaces) {
if (t.prefix == null || t.prefix.isEmpty()) {
return t.uri;
}
}
return inScopeNamespaces.getURIForPrefix("", true);
}
private String getUriForPrefix(String prefix) {
for (Triple t : pendingTag.namespaces) {
if (prefix.equals(t.prefix)) {
return t.uri;
}
}
return inScopeNamespaces.getURIForPrefix(prefix, false);
}
private String getPrefixForUri(String uri) {
for (Triple t : pendingTag.namespaces) {
if (uri.equals(t.uri)) {
return t.prefix == null ? "" : t.prefix;
}
}
String setPrefix = getPrefix(uri);
if (setPrefix != null) {
return setPrefix;
}
Iterator prefixes = inScopeNamespaces.iteratePrefixes();
while (prefixes.hasNext()) {
String p = prefixes.next();
if (inScopeNamespaces.getURIForPrefix(p, false).equals(uri)) {
return p;
}
}
return "";
}
/**
* Generate a start element event for an element in no namespace. Note: the element
* will be in no namespace, even if {@link #setDefaultNamespace(String)} has been called;
* this is Saxon's interpretation of the intended effect of the StAX specification.
*
* @param localName local name of the tag, may not be null
* @throws XMLStreamException if names are being checked and the name is invalid, or if an error occurs downstream
* @throws NullPointerException if the supplied local name is null
*/
public void writeStartElement(String localName) throws XMLStreamException {
checkNonNull(localName);
setPrefixes.push(new ArrayList());
flushStartTag();
depth++;
pendingTag = new StartTag();
pendingTag.elementName.local = localName;
}
/**
* Generate a start element event. The name of the element is determined by the supplied
* namespace URI and local name. The prefix used for the element is determined by the in-scope
* prefixes established using {@link #setPrefix(String, String)} and/or {@link #setDefaultNamespace(String)}
* if these include the specified namespace URI; otherwise the namespace will become the default namespace
* and there will therefore be no prefix.
*
* @param namespaceURI the namespace URI of the element name. Must not be null. A zero-length
* string means the element is in no namespace.
* @param localName local part of the element name. Must not be null
* @throws XMLStreamException if names are being checked and are found to be invalid, or if an
* error occurs downstream in the pipeline.
* @throws NullPointerException if either argument is null
*/
public void writeStartElement(String namespaceURI, String localName) throws XMLStreamException {
checkNonNull(namespaceURI);
checkNonNull(localName);
setPrefixes.push(new ArrayList());
flushStartTag();
depth++;
pendingTag = new StartTag();
pendingTag.elementName.local = localName;
pendingTag.elementName.uri = namespaceURI;
}
/**
* Generate a start element event. The name of the element is determined by the supplied
* namespace URI and local name, and the prefix will be as supplied in the call.
*
* @param prefix the prefix of the element, must not be null. If the prefix is supplied as a zero-length
* string, the element will nave no prefix (that is, the namespace URI will become the default
* namespace).
* @param localName local name of the element, must not be null
* @param namespaceURI the uri to bind the prefix to, must not be null. If the value is a zero-length string,
* the element will be in no namespace; in this case any prefix is ignored.
* @throws NullPointerException if any of the arguments is null.
* @throws XMLStreamException if names are being checked and are found to be invalid, or if an
* error occurs downstream in the pipeline.
*/
public void writeStartElement(String prefix, String localName, String namespaceURI) throws XMLStreamException {
checkNonNull(prefix);
checkNonNull(localName);
checkNonNull(namespaceURI);
setPrefixes.push(new ArrayList());
flushStartTag();
depth++;
pendingTag = new StartTag();
pendingTag.elementName.local = localName;
pendingTag.elementName.uri = namespaceURI;
pendingTag.elementName.prefix = prefix;
}
public void writeEmptyElement(String namespaceURI, String localName) throws XMLStreamException {
checkNonNull(namespaceURI);
checkNonNull(localName);
flushStartTag();
writeStartElement(namespaceURI, localName);
isEmptyElement = true;
}
public void writeEmptyElement(String prefix, String localName, String namespaceURI) throws XMLStreamException {
checkNonNull(prefix);
checkNonNull(localName);
checkNonNull(namespaceURI);
flushStartTag();
writeStartElement(prefix, localName, namespaceURI);
isEmptyElement = true;
}
public void writeEmptyElement(String localName) throws XMLStreamException {
checkNonNull(localName);
flushStartTag();
writeStartElement(localName);
isEmptyElement = true;
}
public void writeEndElement() throws XMLStreamException {
if (depth <= 0) {
throw new IllegalStateException("writeEndElement with no matching writeStartElement");
}
// if (isEmptyElement) {
// throw new IllegalStateException("writeEndElement called for an empty element");
// }
try {
flushStartTag();
setPrefixes.pop();
receiver.endElement();
depth--;
} catch (XPathException err) {
throw new XMLStreamException(err);
}
}
public void writeEndDocument() throws XMLStreamException {
if (depth == -1) {
throw new IllegalStateException("writeEndDocument with no matching writeStartDocument");
}
try {
flushStartTag();
while (depth > 0) {
writeEndElement();
}
receiver.endDocument();
depth = -1;
} catch (XPathException err) {
throw new XMLStreamException(err);
}
}
public void close() throws XMLStreamException {
if (depth >= 0) {
writeEndDocument();
}
try {
receiver.close();
} catch (XPathException err) {
throw new XMLStreamException(err);
}
}
public void flush() throws XMLStreamException {
// no action
}
public void writeAttribute(String localName, String value) throws XMLStreamException {
checkNonNull(localName);
checkNonNull(value);
if (pendingTag == null) {
throw new IllegalStateException("Cannot write attribute when not in a start tag");
}
Triple t = new Triple();
t.local = localName;
t.value = value;
pendingTag.attributes.add(t);
}
public void writeAttribute(String prefix, String namespaceURI, String localName, String value)
throws XMLStreamException {
checkNonNull(prefix);
checkNonNull(namespaceURI);
checkNonNull(localName);
checkNonNull(value);
if (pendingTag == null) {
throw new IllegalStateException("Cannot write attribute when not in a start tag");
}
Triple t = new Triple();
t.prefix = prefix;
t.uri = namespaceURI;
t.local = localName;
t.value = value;
pendingTag.attributes.add(t);
}
public void writeAttribute(String namespaceURI, String localName, String value) throws XMLStreamException {
checkNonNull(namespaceURI);
checkNonNull(localName);
checkNonNull(value);
Triple t = new Triple();
t.uri = namespaceURI;
t.local = localName;
t.value = value;
pendingTag.attributes.add(t);
}
/**
* Emits a namespace declaration event.
*
* If the prefix argument to this method is the empty string,
* "xmlns", or null this method will delegate to writeDefaultNamespace.
*
* This method does not change the name of any element or attribute; its only use is to write
* additional or redundant namespace declarations. With this implementation of XMLStreamWriter,
* this method is needed only to generate namespace declarations for prefixes that do not appear
* in element or attribute names. If an attempt is made to generate a namespace declaration that
* conflicts with the prefix-uri bindings in scope for element and attribute names, an exception
* occurs.
*
* @param prefix the prefix to bind this namespace to
* @param namespaceURI the uri to bind the prefix to
* @throws IllegalStateException if the current state does not allow Namespace writing
* @throws XMLStreamException
*/
public void writeNamespace(String prefix, String namespaceURI) throws XMLStreamException {
if (prefix == null || prefix.equals("") || prefix.equals("xmlns")) {
writeDefaultNamespace(namespaceURI);
} else {
checkNonNull(namespaceURI);
if (pendingTag == null) {
throw new IllegalStateException("Cannot write namespace when not in a start tag");
}
Triple t = new Triple();
t.uri = namespaceURI;
t.prefix = prefix;
pendingTag.namespaces.add(t);
}
}
/**
* Emits a default namespace declaration
*
* This method does not change the name of any element or attribute; its only use is to write
* additional or redundant namespace declarations. With this implementation of XMLStreamWriter,
* this method is needed only to generate namespace declarations for prefixes that do not appear
* in element or attribute names. If an attempt is made to generate a namespace declaration that
* conflicts with the prefix-uri bindings in scope for element and attribute names, an exception
* occurs.
*
* @param namespaceURI the uri to bind the default namespace to
* @throws IllegalStateException if the current state does not allow Namespace writing
* @throws XMLStreamException
*/
public void writeDefaultNamespace(String namespaceURI)
throws XMLStreamException {
checkNonNull(namespaceURI);
if (pendingTag == null) {
throw new IllegalStateException("Cannot write namespace when not in a start tag");
}
Triple t = new Triple();
t.uri = namespaceURI;
pendingTag.namespaces.add(t);
}
public void writeComment(String data) throws XMLStreamException {
flushStartTag();
if (data == null) {
data = "";
}
try {
if (!isValidChars(data)) {
throw new IllegalArgumentException("Invalid XML character in comment: " + data);
}
if (isChecking && data.contains("--")) {
throw new IllegalArgumentException("Comment contains '--'");
}
receiver.comment(data, ExplicitLocation.UNKNOWN_LOCATION, 0);
} catch (XPathException err) {
throw new XMLStreamException(err);
}
}
public void writeProcessingInstruction(String target) throws XMLStreamException {
writeProcessingInstruction(target, "");
}
public void writeProcessingInstruction(/*@NotNull*/ String target, /*@NotNull*/ String data) throws XMLStreamException {
checkNonNull(target);
checkNonNull(data);
flushStartTag();
try {
if (isChecking) {
if (!isValidNCName(target) || "xml".equalsIgnoreCase(target)) {
throw new IllegalArgumentException("Invalid PITarget: " + target);
}
if (!isValidChars(data)) {
throw new IllegalArgumentException("Invalid character in PI data: " + data);
}
}
receiver.processingInstruction(target, data, ExplicitLocation.UNKNOWN_LOCATION, 0);
} catch (XPathException err) {
throw new XMLStreamException(err);
}
}
public void writeCData(/*@NotNull*/ String data) throws XMLStreamException {
checkNonNull(data);
flushStartTag();
writeCharacters(data);
}
public void writeDTD(String dtd) throws XMLStreamException {
// no-op
}
public void writeEntityRef(String name) throws XMLStreamException {
throw new UnsupportedOperationException("writeEntityRef");
}
public void writeStartDocument() throws XMLStreamException {
writeStartDocument("utf-8", "1.0");
}
public void writeStartDocument(/*@Nullable*/ String version) throws XMLStreamException {
writeStartDocument("utf-8", version);
}
public void writeStartDocument(/*@Nullable*/ String encoding, /*@Nullable*/ String version) throws XMLStreamException {
if (encoding == null) {
encoding = "utf-8";
}
if (version == null) {
version = "1.0";
}
if (depth != -1) {
throw new IllegalStateException("writeStartDocument must be the first call");
}
try {
receiver.open();
receiver.startDocument(0);
} catch (XPathException err) {
throw new XMLStreamException(err);
}
depth = 0;
}
public void writeCharacters(String text)
throws XMLStreamException {
checkNonNull(text);
flushStartTag();
if (!isValidChars(text)) {
throw new IllegalArgumentException("illegal XML character: " + text);
}
try {
receiver.characters(text, ExplicitLocation.UNKNOWN_LOCATION, 0);
} catch (XPathException err) {
throw new XMLStreamException(err);
}
}
public void writeCharacters(char[] text, int start, int len)
throws XMLStreamException {
checkNonNull(text);
writeCharacters(new String(text, start, len));
}
public String getPrefix(String uri) {
for (int i=setPrefixes.size()-1; i>=0; i--) {
List bindings = setPrefixes.get(i);
for (int j=bindings.size()-1; j>=0; j--) {
NamespaceBinding binding = bindings.get(j);
if (binding.getURI().equals(uri)) {
return binding.getPrefix();
}
}
}
if (rootNamespaceContext != null) {
return rootNamespaceContext.getPrefix(uri);
}
return null;
}
public void setPrefix(String prefix, String uri) {
// See Saxon bug 2398: this should have stack-like effect
checkNonNull(prefix);
if (uri == null) {
uri = "";
}
if (!isValidURI(uri)) {
throw new IllegalArgumentException("Invalid namespace URI: " + uri);
}
if (!"".equals(prefix) && !isValidNCName(prefix)) {
throw new IllegalArgumentException("Invalid namespace prefix: " + prefix);
}
setPrefixes.peek().add(new NamespaceBinding(prefix, uri));
}
public void setDefaultNamespace(String uri)
throws XMLStreamException {
setPrefix("", uri);
}
public void setNamespaceContext(javax.xml.namespace.NamespaceContext context)
throws XMLStreamException {
// Note, we do not enforce the rule that this can only be called once, because the spec is self-contradictory
// on this point.
if (depth > 0) {
throw new IllegalStateException("setNamespaceContext may only be called at the start of the document");
}
// Unfortunately the JAXP NamespaceContext class does not allow us to discover all the namespaces
// that were declared, nor to declare new ones. So we have to retain it separately
rootNamespaceContext = context;
}
/**
* Return the current namespace context.
*
* The specification of this method is hopelessly vague. This method returns a namespace context
* that contains the namespaces declared using {@link #setPrefix(String, String)} calls that are
* in-scope at the time, overlaid on the root namespace context that was defined using
* {@link #setNamespaceContext(NamespaceContext)}. The namespaces bound using {@link #setPrefix(String, String)}
* are copied, and are therefore unaffected by subsequent changes, but the root namespace context
* is not copied, because the NamespaceContext interface provides no way of doing so.
* @return a copy of the current namespace context.
*/
public javax.xml.namespace.NamespaceContext getNamespaceContext() {
return new NamespaceContext() {
final NamespaceContext rootNamespaceContext = StreamWriterToReceiver.this.rootNamespaceContext;
final Map bindings = new HashMap();
{
for (List list : setPrefixes) {
for (NamespaceBinding binding : list) {
bindings.put(binding.getPrefix(), binding.getURI());
}
}
}
public String getNamespaceURI(String prefix) {
String uri = bindings.get(prefix);
if (uri != null) {
return uri;
}
return rootNamespaceContext.getNamespaceURI(prefix);
}
public String getPrefix(String namespaceURI) {
for (Map.Entry entry : bindings.entrySet()) {
if (entry.getValue().equals(namespaceURI)) {
return entry.getKey();
}
}
return rootNamespaceContext.getPrefix(namespaceURI);
}
public Iterator getPrefixes(String namespaceURI) {
List prefixes = new ArrayList();
for (Map.Entry entry : bindings.entrySet()) {
if (entry.getValue().equals(namespaceURI)) {
prefixes.add(entry.getKey());
}
}
Iterator root = rootNamespaceContext.getPrefixes(namespaceURI);
while (root.hasNext()) {
prefixes.add(root.next());
}
return prefixes.iterator();
}
};
}
public Object getProperty(String name) throws IllegalArgumentException {
if (name.equals("javax.xml.stream.isRepairingNamespaces")) {
return receiver instanceof NamespaceReducer;
} else {
throw new IllegalArgumentException(name);
}
}
/**
* Test whether a supplied name is a valid NCName
*
* @param name the name to be tested
* @return true if the name is valid or if checking is disabled
*/
private boolean isValidNCName(String name) {
return !isChecking || NameChecker.isValidNCName(name);
}
/**
* Test whether a supplied character string is valid in XML
*
* @param text the string to be tested
* @return true if the string is valid or if checking is disabled
*/
private boolean isValidChars(String text) {
return !isChecking || (UTF16CharacterSet.firstInvalidChar(text, charChecker) == -1);
}
/**
* Test whether a supplied namespace URI is a valid URI
*
* @param uri the namespace URI to be tested
* @return true if the name is valid or if checking is disabled
*/
private boolean isValidURI(String uri) {
return !isChecking || StandardURIChecker.getInstance().isValidURI(uri);
}
private void checkNonNull(Object value) {
if (value == null) {
throw new NullPointerException();
}
}
}