net.sf.saxon.event.ComplexContentOutputter Maven / Gradle / Ivy
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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.Location;
import net.sf.saxon.ma.arrays.ArrayItem;
import net.sf.saxon.ma.map.MapItem;
import net.sf.saxon.Configuration;
import net.sf.saxon.expr.parser.ExplicitLocation;
import net.sf.saxon.lib.ParseOptions;
import net.sf.saxon.lib.Validation;
import net.sf.saxon.om.*;
import net.sf.saxon.trans.Err;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.type.BuiltInAtomicType;
import net.sf.saxon.type.SchemaType;
import net.sf.saxon.type.SimpleType;
import net.sf.saxon.type.Type;
import net.sf.saxon.value.AtomicValue;
import net.sf.saxon.value.ObjectValue;
/**
* This class is used for generating complex content, that is, the content of an
* element or document node. It enforces the rules on the order of events within
* complex content (attributes and namespaces must come first), and it implements
* part of the namespace fixup rules, in particular, it ensures that there is a
* namespace node for the namespace used in the element name and in each attribute
* name.
*
* The same ComplexContentOutputter may be used for generating an entire XML
* document; it is not necessary to create a new outputter for each element node.
*
* @author Michael H. Kay
*/
public final class ComplexContentOutputter extends SequenceReceiver {
private Receiver nextReceiver;
// the next receiver in the output pipeline
private int pendingStartTagDepth = -2;
// -2 means we are at the top level, or immediately within a document node
// -1 means we are in the content of an element node whose start tag is complete
private NodeName pendingStartTag = null;
private int level = -1; // records the number of startDocument or startElement events
// that have not yet been closed. Note that startDocument and startElement
// events may be arbitrarily nested; startDocument and endDocument
// are ignored unless they occur at the outermost level, except that they
// still change the level number
private boolean[] currentLevelIsDocument = new boolean[20];
private Boolean elementIsInNullNamespace;
private NodeName[] pendingAttCode = new NodeName[20];
private SimpleType[] pendingAttType = new SimpleType[20];
private String[] pendingAttValue = new String[20];
private Location[] pendingAttLocation = new Location[20];
private int[] pendingAttProp = new int[20];
private int pendingAttListSize = 0;
private NamespaceBinding[] pendingNSList = new NamespaceBinding[20];
private int pendingNSListSize = 0;
private SchemaType currentSimpleType = null; // any other value means we are currently writing an
// element of a particular simple type
private int startElementProperties;
private Location startElementLocationId = ExplicitLocation.UNKNOWN_LOCATION;
private boolean declaresDefaultNamespace;
private int hostLanguage = Configuration.XSLT;
private boolean serializing = false;
/**
* Create a ComplexContentOutputter
*
* @param pipe the pipeline configuration
*/
public ComplexContentOutputter(/*@NotNull*/ PipelineConfiguration pipe) {
super(pipe);
//System.err.println("ComplexContentOutputter init");
}
/**
* Static factory method to create an push pipeline containing a ComplexContentOutputter
* @param receiver the destination to which the constructed complex content will be written
* @param options options for validating the output stream; may be null
* @throws XPathException if any dynamic error occurs; and
* specifically, if an attempt is made to switch to a final output
* destination while writing a temporary tree or sequence @param isFinal true if the destination is a final result tree
* (either the principal output or a secondary result tree); false if @param validation Validation to be performed on the output document
*/
public static SequenceReceiver makeComplexContentReceiver(Receiver receiver, ParseOptions options)
throws XPathException {
// System.err.println("CHANGE OUTPUT DESTINATION new=" + receiver);
PipelineConfiguration pipe = receiver.getPipelineConfiguration();
ComplexContentOutputter out = new ComplexContentOutputter(pipe);
out.setHostLanguage(pipe.getHostLanguage());
// add a filter to remove duplicate namespaces
NamespaceReducer ne = new NamespaceReducer(receiver);
ne.setSystemId(receiver.getSystemId());
receiver = ne;
// add a validator to the pipeline if required
if (options != null && options.getSchemaValidationMode() != Validation.PRESERVE) {
Configuration config = pipe.getConfiguration();
receiver = config.getDocumentValidator(ne, receiver.getSystemId(), options);
}
out.setReceiver(receiver);
return out;
}
public void setPipelineConfiguration(/*@NotNull*/ PipelineConfiguration pipe) {
if (pipelineConfiguration != pipe) {
pipelineConfiguration = pipe;
if (nextReceiver != null) {
nextReceiver.setPipelineConfiguration(pipe);
}
}
}
@Override
public void setSystemId(String systemId) {
super.setSystemId(systemId);
nextReceiver.setSystemId(systemId);
}
/**
* Set the host language
*
* @param language the host language, for example {@link Configuration#XQUERY}
*/
public void setHostLanguage(int language) {
hostLanguage = language;
}
/**
* Say whether this ComplexContentOutputter is performing serialization
* @param serializing true if this class implements the sequence normalization function of the serialization
* spec; false if it is constructing document or element nodes in XSLT or XQuery
*/
public void setSerializing(boolean serializing) {
this.serializing = serializing;
}
/**
* Ask whether this ComplexContentOutputter is performing serialization
*
* @return true if this class implements the sequence normalization function of the serialization
* spec; false if it is constructing document or element nodes in XSLT or XQuery
*/
public boolean isSerializing() {
return serializing;
}
/**
* Set the receiver (to handle the next stage in the pipeline) directly
*
* @param receiver the receiver to handle the next stage in the pipeline
*/
public void setReceiver(Receiver receiver) {
this.nextReceiver = receiver;
}
/**
* Start the output process
*/
public void open() throws XPathException {
nextReceiver.open();
previousAtomic = false;
}
/**
* Start of a document node.
*/
public void startDocument(int properties) throws XPathException {
level++;
if (level == 0) {
nextReceiver.startDocument(properties);
} else if (pendingStartTagDepth >= 0) {
startContent();
pendingStartTagDepth = -2;
}
previousAtomic = false;
if (currentLevelIsDocument.length < level + 1) {
boolean[] b2 = new boolean[level * 2];
System.arraycopy(currentLevelIsDocument, 0, b2, 0, level);
currentLevelIsDocument = b2;
}
currentLevelIsDocument[level] = true;
}
/**
* Notify the end of a document node
*/
public void endDocument() throws XPathException {
if (level == 0) {
nextReceiver.endDocument();
}
previousAtomic = false;
level--;
}
/**
* Produce text content output.
* Special characters are escaped using XML/HTML conventions if the output format
* requires it.
*
* @param s The String to be output
* @param locationId
* @throws XPathException for any failure
*/
public void characters(CharSequence s, Location locationId, int properties) throws XPathException {
previousAtomic = false;
if (s == null) {
return;
}
int len = s.length();
if (len == 0) {
return;
}
if (pendingStartTagDepth >= 0) {
startContent();
}
nextReceiver.characters(s, locationId, properties);
}
/**
* Output an element start tag.
* The actual output of the tag is deferred until all attributes have been output
* using attribute().
*
* @param elemName The element name
* @param location
*/
public void startElement(NodeName elemName, SchemaType typeCode, Location location, int properties) throws XPathException {
//System.err.println("CCO " + this + "StartElement " + nameCode);
level++;
if (pendingStartTagDepth >= 0) {
startContent();
}
startElementProperties = properties;
startElementLocationId = location.saveLocation();
pendingAttListSize = 0;
pendingNSListSize = 0;
pendingStartTag = elemName;
pendingStartTagDepth = 1;
elementIsInNullNamespace = null; // meaning not yet computed
declaresDefaultNamespace = false;
currentSimpleType = typeCode;
previousAtomic = false;
if (currentLevelIsDocument.length < level + 1) {
boolean[] b2 = new boolean[level * 2];
System.arraycopy(currentLevelIsDocument, 0, b2, 0, level);
currentLevelIsDocument = b2;
}
currentLevelIsDocument[level] = false;
}
/**
* Output a namespace declaration.
* This is added to a list of pending namespaces for the current start tag.
* If there is already another declaration of the same prefix, this one is
* ignored, unless the REJECT_DUPLICATES flag is set, in which case this is an error.
* Note that unlike SAX2 startPrefixMapping(), this call is made AFTER writing the start tag.
*
* @param nsBinding The namespace binding
* @throws XPathException if there is no start tag to write to (created using writeStartTag),
* or if character content has been written since the start tag was written.
*/
public void namespace(NamespaceBinding nsBinding, int properties)
throws XPathException {
// System.err.println("Write namespace prefix=" + (nscode>>16) + " uri=" + (nscode&0xffff));
if (pendingStartTagDepth < 0) {
throw NoOpenStartTagException.makeNoOpenStartTagException(
Type.NAMESPACE,
nsBinding.getPrefix(),
hostLanguage,
pendingStartTagDepth == -2,
isSerializing(),
startElementLocationId);
}
// elimination of namespaces already present on an outer element of the
// result tree is done by the NamespaceReducer.
// Handle declarations whose prefix is duplicated for this element.
boolean rejectDuplicates = (properties & ReceiverOptions.REJECT_DUPLICATES) != 0;
for (int i = 0; i < pendingNSListSize; i++) {
if (nsBinding.equals(pendingNSList[i])) {
// same prefix and URI: ignore this duplicate
return;
}
if (nsBinding.getPrefix().equals(pendingNSList[i].getPrefix())) {
if (pendingNSList[i].isDefaultUndeclaration() || nsBinding.isDefaultUndeclaration()) {
// xmlns="" overridden by xmlns="abc"
pendingNSList[i] = nsBinding;
} else if (rejectDuplicates) {
String prefix = nsBinding.getPrefix();
String uri1 = nsBinding.getURI();
String uri2 = pendingNSList[i].getURI();
XPathException err = new XPathException("Cannot create two namespace nodes with the same prefix mapped to different URIs (prefix=" +
(prefix.isEmpty() ? "\"\"" : prefix) + ", URI=" +
(uri1.isEmpty() ? "\"\"" : uri1) + ", URI=" +
(uri2.isEmpty() ? "\"\"" : uri2) + ")");
err.setErrorCode(hostLanguage == Configuration.XSLT ? "XTDE0430" : "XQDY0102");
throw err;
} else {
// same prefix, do a quick exit
return;
}
}
}
// It is an error to output a namespace node for the default namespace if the element
// itself is in the null namespace, as the resulting element could not be serialized
if (nsBinding.getPrefix().isEmpty() && (nsBinding.getURI().length() != 0)) {
declaresDefaultNamespace = true;
if (elementIsInNullNamespace == null) {
elementIsInNullNamespace = pendingStartTag.hasURI("");
}
if (elementIsInNullNamespace) {
XPathException err = new XPathException("Cannot output a namespace node for the default namespace when the element is in no namespace");
err.setErrorCode(hostLanguage == Configuration.XSLT ? "XTDE0440" : "XQDY0102");
throw err;
}
}
// if it's not a duplicate namespace, add it to the list for this start tag
if (pendingNSListSize + 1 > pendingNSList.length) {
NamespaceBinding[] newlist = new NamespaceBinding[pendingNSListSize * 2];
System.arraycopy(pendingNSList, 0, newlist, 0, pendingNSListSize);
pendingNSList = newlist;
}
pendingNSList[pendingNSListSize++] = nsBinding;
previousAtomic = false;
}
/**
* Output an attribute value.
* This is added to a list of pending attributes for the current start tag, overwriting
* any previous attribute with the same name.
* This method should NOT be used to output namespace declarations.
*
* @param attName The name of the attribute
* @param value The value of the attribute
* @param locationId
*@param properties Bit fields containing properties of the attribute to be written @throws XPathException if there is no start tag to write to (created using writeStartTag),
* or if character content has been written since the start tag was written.
*/
public void attribute(NodeName attName, SimpleType typeCode, CharSequence value, Location locationId, int properties) throws XPathException {
//System.err.println("Write attribute " + nameCode + "=" + value + " to Outputter " + this);
if (pendingStartTagDepth < 0) {
// The complexity here is in identifying the right error message and error code
XPathException err = NoOpenStartTagException.makeNoOpenStartTagException(
Type.ATTRIBUTE,
attName.getDisplayName(),
hostLanguage,
level < 0 || currentLevelIsDocument[level],
isSerializing(),
startElementLocationId);
err.setLocator(locationId);
throw err;
}
// if this is a duplicate attribute, overwrite the original, unless
// the REJECT_DUPLICATES option is set.
for (int a = 0; a < pendingAttListSize; a++) {
if (pendingAttCode[a].equals(attName)) {
if (hostLanguage == Configuration.XSLT) {
pendingAttType[a] = typeCode;
pendingAttValue[a] = value.toString();
// we have to copy the CharSequence, because some kinds of CharSequence are mutable.
pendingAttLocation[a] = locationId;
pendingAttProp[a] = properties;
return;
} else {
XPathException err = new XPathException("Cannot create an element having two attributes with the same name: " +
Err.wrap(attName.getDisplayName(), Err.ATTRIBUTE));
err.setErrorCode("XQDY0025");
throw err;
}
}
}
// for top-level attributes (attributes whose parent element is not being copied),
// check that the type annotation is not namespace-sensitive (because the namespace context might
// be different, and we don't do namespace fixup for prefixes in content: see bug 4151
if (level == 0 && !typeCode.equals(BuiltInAtomicType.UNTYPED_ATOMIC) /**/ && currentLevelIsDocument[0] /**/) {
// commenting-out in line above done MHK 22 Jul 2011 to pass test Constr-cont-nsmode-8
// reverted 2011-07-27 to pass tests in qischema family
if (typeCode.isNamespaceSensitive()) {
XPathException err = new XPathException("Cannot copy attributes whose type is namespace-sensitive (QName or NOTATION): " +
Err.wrap(attName.getDisplayName(), Err.ATTRIBUTE));
err.setErrorCode(hostLanguage == Configuration.XSLT ? "XTTE0950" : "XQTY0086");
throw err;
}
}
// otherwise, add this one to the list
if (pendingAttListSize >= pendingAttCode.length) {
NodeName[] attCode2 = new NodeName[pendingAttListSize * 2];
SimpleType[] attType2 = new SimpleType[pendingAttListSize * 2];
String[] attValue2 = new String[pendingAttListSize * 2];
Location[] attLoc2 = new Location[pendingAttListSize * 2];
int[] attProp2 = new int[pendingAttListSize * 2];
System.arraycopy(pendingAttCode, 0, attCode2, 0, pendingAttListSize);
System.arraycopy(pendingAttType, 0, attType2, 0, pendingAttListSize);
System.arraycopy(pendingAttValue, 0, attValue2, 0, pendingAttListSize);
System.arraycopy(pendingAttLocation, 0, attLoc2, 0, pendingAttListSize);
System.arraycopy(pendingAttProp, 0, attProp2, 0, pendingAttListSize);
pendingAttCode = attCode2;
pendingAttType = attType2;
pendingAttValue = attValue2;
pendingAttLocation = attLoc2;
pendingAttProp = attProp2;
}
pendingAttCode[pendingAttListSize] = attName;
pendingAttType[pendingAttListSize] = typeCode;
pendingAttValue[pendingAttListSize] = value.toString();
pendingAttLocation[pendingAttListSize] = locationId;
pendingAttProp[pendingAttListSize] = properties;
pendingAttListSize++;
previousAtomic = false;
}
/**
* Check that the prefix for an element or attribute is acceptable, allocating a substitute
* prefix if not. The prefix is acceptable unless a namespace declaration has been
* written that assignes this prefix to a different namespace URI. This method
* also checks that the element or attribute namespace has been declared, and declares it
* if not.
*
* @param nodeName the proposed name, including proposed prefix
* @param seq sequence number, used for generating a substitute prefix when necessary
* @return a nameCode to use in place of the proposed nameCode (or the original nameCode
* if no change is needed)
* @throws net.sf.saxon.trans.XPathException
* if an error occurs writing the new
* namespace node
*/
private NodeName checkProposedPrefix(NodeName nodeName, int seq) throws XPathException {
NamespaceBinding binding = nodeName.getNamespaceBinding();
String nsprefix = binding.getPrefix();
for (int i = 0; i < pendingNSListSize; i++) {
if (nsprefix.equals(pendingNSList[i].getPrefix())) {
// same prefix
if (binding.getURI().equals(pendingNSList[i].getURI())) {
// same URI
return nodeName; // all is well
} else {
String prefix = getSubstitutePrefix(binding, seq);
NodeName newName = new FingerprintedQName(prefix, nodeName.getURI(), nodeName.getLocalPart());
namespace(newName.getNamespaceBinding(), 0);
return newName;
}
}
}
// no declaration of this prefix: declare it now
namespace(binding, 0);
return nodeName;
}
/**
* It is possible for a single output element to use the same prefix to refer to different
* namespaces. In this case we have to generate an alternative prefix for uniqueness. The
* one we generate is based on the sequential position of the element/attribute: this is
* designed to ensure both uniqueness (with a high probability) and repeatability
*
* @param nscode the proposed namespace code
* @param seq sequence number for use in the substitute prefix
* @return a prefix to use in place of the one originally proposed
*/
private String getSubstitutePrefix(NamespaceBinding nscode, int seq) {
return nscode.getPrefix() + '_' + seq;
}
/**
* Output an element end tag.
*/
public void endElement() throws XPathException {
//System.err.println("Write end tag " + this + " : " + name);
if (pendingStartTagDepth >= 0) {
startContent();
} else {
pendingStartTagDepth = -2;
pendingStartTag = null;
}
// write the end tag
nextReceiver.endElement();
level--;
previousAtomic = false;
}
/**
* Write a comment
*/
public void comment(CharSequence comment, Location locationId, int properties) throws XPathException {
if (pendingStartTagDepth >= 0) {
startContent();
}
nextReceiver.comment(comment, locationId, properties);
previousAtomic = false;
}
/**
* Write a processing instruction
*/
public void processingInstruction(String target, CharSequence data, Location locationId, int properties) throws XPathException {
if (pendingStartTagDepth >= 0) {
startContent();
}
nextReceiver.processingInstruction(target, data, locationId, properties);
previousAtomic = false;
}
/**
* Append an arbitrary item (node or atomic value) to the output
* @param item the item to be appended
* @param locationId the location of the calling instruction, for diagnostics
* @param copyNamespaces if the item is an element node, this indicates whether its namespaces
* need to be copied. Values are {@link NodeInfo#ALL_NAMESPACES},
* {@link NodeInfo#LOCAL_NAMESPACES}, {@link NodeInfo#NO_NAMESPACES}
*/
public void append(Item item, Location locationId, int copyNamespaces) throws XPathException {
if (item != null) {
if (item instanceof AtomicValue || item instanceof ObjectValue) {
if (previousAtomic) {
characters(" ", locationId, 0);
}
characters(item.getStringValueCS(), locationId, 0);
previousAtomic = true;
} else if (item instanceof Function) {
if (SequenceReceiver.isTrueSequenceReceiver(nextReceiver)) {
((SequenceReceiver)nextReceiver).append(item);
} else {
String kind = "a function item";
if (item instanceof MapItem) {
kind = "a map";
} else if (item instanceof ArrayItem) {
kind = "an array";
}
boolean isXSLT = getPipelineConfiguration().getHostLanguage() == Configuration.XSLT;
throw new XPathException("Cannot add " + kind + " to an XML tree", isXSLT ? "XTDE0450" : "FOTY0013");
}
} else if (((NodeInfo) item).getNodeKind() == Type.DOCUMENT) {
startDocument(0);
SequenceIterator iter = ((NodeInfo) item).iterateAxis(AxisInfo.CHILD);
Item it;
while ((it = iter.next()) != null) {
append(it, locationId, copyNamespaces);
}
endDocument();
previousAtomic = false;
} else {
int copyOptions = CopyOptions.TYPE_ANNOTATIONS;
if (copyNamespaces == NodeInfo.LOCAL_NAMESPACES) {
copyOptions |= CopyOptions.LOCAL_NAMESPACES;
} else if (copyNamespaces == NodeInfo.ALL_NAMESPACES) {
copyOptions |= CopyOptions.ALL_NAMESPACES;
}
((NodeInfo) item).copy(this, copyOptions, locationId);
previousAtomic = false;
}
}
}
/**
* Close the output
*/
public void close() throws XPathException {
// System.err.println("Close " + this + " using emitter " + emitter.getClass());
nextReceiver.close();
previousAtomic = false;
}
/**
* Flush out a pending start tag
*/
public void startContent() throws XPathException {
if (pendingStartTagDepth < 0) {
// this can happen if the method is called from outside,
// e.g. from a SequenceOutputter earlier in the pipeline
return;
}
int props = startElementProperties;
NodeName elcode = pendingStartTag;
if (declaresDefaultNamespace || pendingStartTag.getPrefix().length() != 0) {
// skip this check if the element is unprefixed and no xmlns="abc" declaration has been encountered
elcode = checkProposedPrefix(pendingStartTag, 0);
props = startElementProperties | ReceiverOptions.NAMESPACE_OK;
}
nextReceiver.startElement(elcode, currentSimpleType, startElementLocationId, props);
for (int a = 0; a < pendingAttListSize; a++) {
NodeName attcode = pendingAttCode[a];
if (!attcode.hasURI("")) { // non-null prefix
attcode = checkProposedPrefix(attcode, a + 1);
pendingAttCode[a] = attcode;
}
}
for (int n = 0; n < pendingNSListSize; n++) {
nextReceiver.namespace(pendingNSList[n], 0);
}
for (int a = 0; a < pendingAttListSize; a++) {
nextReceiver.attribute(pendingAttCode[a],
pendingAttType[a],
pendingAttValue[a],
pendingAttLocation[a],
pendingAttProp[a]);
}
nextReceiver.startContent();
pendingAttListSize = 0;
pendingNSListSize = 0;
pendingStartTagDepth = -1;
previousAtomic = false;
}
/**
* Ask whether this Receiver (or the downstream pipeline) makes any use of the type annotations
* supplied on element and attribute events
*
* @return true if the Receiver makes any use of this information. If false, the caller
* may supply untyped nodes instead of supplying the type annotation
*/
public boolean usesTypeAnnotations() {
return nextReceiver.usesTypeAnnotations();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy