net.sf.saxon.serialize.XMLEmitter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of Saxon-HE Show documentation
Show all versions of Saxon-HE Show documentation
The XSLT and XQuery Processor
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2018-2022 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.serialize;
import net.sf.saxon.Configuration;
import net.sf.saxon.event.ReceiverOption;
import net.sf.saxon.lib.SaxonOutputKeys;
import net.sf.saxon.om.*;
import net.sf.saxon.s9api.Location;
import net.sf.saxon.serialize.charcode.UTF8CharacterSet;
import net.sf.saxon.str.*;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.type.SchemaType;
import net.sf.saxon.value.Whitespace;
import net.sf.saxon.z.IntIterator;
import javax.xml.transform.OutputKeys;
import java.io.IOException;
import java.util.Arrays;
import java.util.Properties;
import java.util.Stack;
import java.util.function.IntPredicate;
/**
* XMLEmitter is an Emitter that generates XML output
* to a specified destination.
*/
public class XMLEmitter extends Emitter {
// NOTE: we experimented with XMLUTF8Emitter which combines XML escaping and UTF8 encoding
// into a single loop. Scrapped it because we couldn't measure any benefits - but there
// ought to be, in theory. Perhaps we weren't buffering the writes carefully enough.
protected boolean canonical = false;
protected boolean started = false;
protected boolean startedElement = false;
protected boolean openStartTag = false;
protected boolean declarationIsWritten = false;
protected NodeName elementCode;
protected int indentForNextAttribute = -1;
protected boolean undeclareNamespaces = false;
protected boolean unfailing = false;
protected String internalSubset = null;
protected char delimiter = '"';
protected boolean[] attSpecials = specialInAtt;
// The element stack holds the display names (lexical QNames) of elements that
// have been started but not finished. It is used to obtain the element name
// for the end tag.
protected Stack elementStack = new Stack<>();
// For other names we use a hashtable. It
private boolean indenting = false;
private boolean requireWellFormed = false;
protected CharacterReferenceGenerator characterReferenceGenerator = HexCharacterReferenceGenerator.THE_INSTANCE;
protected static boolean[] specialInText; // lookup table for special characters in text
protected static boolean[] specialInAtt; // lookup table for special characters in attributes
protected static boolean[] specialInAttSingle; // lookup table for special characters in attributes with single-quote delimiter
// create look-up table for ASCII characters that need special treatment
static {
specialInText = new boolean[128];
for (int i = 0; i <= 31; i++) {
specialInText[i] = true; // allowed in XML 1.1 as character references
}
for (int i = 32; i <= 127; i++) {
specialInText[i] = false;
}
// note, 0 is used to switch escaping on and off for mapped characters
specialInText['\n'] = false;
specialInText['\t'] = false;
specialInText['\r'] = true;
specialInText['<'] = true;
specialInText['>'] = true;
specialInText['&'] = true;
specialInAtt = new boolean[128];
for (int i = 0; i <= 31; i++) {
specialInAtt[i] = true; // allowed in XML 1.1 as character references
}
for (int i = 32; i <= 127; i++) {
specialInAtt[i] = false;
}
specialInAtt[(char) 0] = true;
// used to switch escaping on and off for mapped characters
specialInAtt['\r'] = true;
specialInAtt['\n'] = true;
specialInAtt['\t'] = true;
specialInAtt['<'] = true;
specialInAtt['>'] = true;
specialInAtt['&'] = true;
specialInAtt['\"'] = true;
specialInAttSingle = Arrays.copyOf(specialInAtt, 128);
specialInAttSingle['\"'] = false;
specialInAttSingle['\''] = true;
}
IntPredicate isSpecialInText;
IntPredicate isSpecialInAttribute;
public XMLEmitter() {
}
/**
* Set the character reference generator to be used for generating hexadecimal or decimal
* character references
*
* @param generator the character reference generator to be used
*/
public void setCharacterReferenceGenerator(CharacterReferenceGenerator generator) {
this.characterReferenceGenerator = generator;
}
/**
* Say that all non-ASCII characters should be escaped, regardless of the character encoding
*
* @param escape true if all non ASCII characters should be escaped
*/
public void setEscapeNonAscii(Boolean escape) {
// no action (not currently supported for this output method
}
/**
* Start of the event stream. Nothing is done at this stage: the opening of the output
* file is deferred until some content is written to it.
*/
@Override
public void open() throws XPathException {
}
/**
* Start of a document node. Nothing is done at this stage: the opening of the output
* file is deferred until some content is written to it.
*/
@Override
public void startDocument(int properties) throws XPathException {
}
/**
* Notify the end of a document node
*/
@Override
public void endDocument() throws XPathException {
// Following code removed as a result of bug 2323. If a failure occurs during xsl:result-document processing,
// and the output is being written to a SAXResult, then the ContentHandler.endDocument() method is called in order
// to close any open files; and this calls Emitter.endDocument() at a point where the output is incomplete.
// if (!elementStack.isEmpty()) {
// throw new IllegalStateException("Attempt to end document in serializer when elements are unclosed");
// }
}
/**
* Do the real work of starting the document. This happens when the first
* content is written.
*
* @throws XPathException if an error occurs opening the output file
*/
protected void openDocument() throws XPathException {
assert writer != null;
// if (writer == null) {
// makeWriter();
// }
if (characterSet == null) {
characterSet = UTF8CharacterSet.getInstance();
}
if (outputProperties == null) {
outputProperties = new Properties();
}
undeclareNamespaces = "yes".equals(outputProperties.getProperty(SaxonOutputKeys.UNDECLARE_PREFIXES));
canonical = "yes".equals(outputProperties.getProperty(SaxonOutputKeys.CANONICAL));
unfailing = "yes".equals(outputProperties.getProperty(SaxonOutputKeys.UNFAILING));
internalSubset = outputProperties.getProperty(SaxonOutputKeys.INTERNAL_DTD_SUBSET);
if ("yes".equals(outputProperties.getProperty(SaxonOutputKeys.SINGLE_QUOTES))) {
delimiter = '\'';
attSpecials = specialInAttSingle;
}
if (allCharactersEncodable) {
isSpecialInText = c -> (c < 127 ? specialInText[c] : (c < 160 || c == 0x2028));
isSpecialInAttribute = c -> (c < 127 ? attSpecials[c] : (c < 160 || c == 0x2028));
} else {
isSpecialInText = c -> (c < 127 ? specialInText[c] : (c < 160 || c == 0x2028 || c > 65535 || !characterSet.inCharset(c)));
isSpecialInAttribute = c -> (c < 127 ? attSpecials[c] : (c < 160 || c == 0x2028 || c > 65535 || !characterSet.inCharset(c)));
}
writeDeclaration();
}
/**
* Output the XML declaration
*
* @throws XPathException if any error occurs
*/
public void writeDeclaration() throws XPathException {
if (declarationIsWritten) {
return;
}
declarationIsWritten = true;
try {
indenting = "yes".equals(outputProperties.getProperty(OutputKeys.INDENT));
String byteOrderMark = outputProperties.getProperty(SaxonOutputKeys.BYTE_ORDER_MARK);
String encoding = outputProperties.getProperty(OutputKeys.ENCODING);
if (encoding == null || encoding.equalsIgnoreCase("utf8") || canonical) {
encoding = "UTF-8";
}
if ("yes".equals(byteOrderMark) && !canonical && (
"UTF-8".equalsIgnoreCase(encoding) ||
"UTF-16LE".equalsIgnoreCase(encoding) ||
"UTF-16BE".equalsIgnoreCase(encoding))) {
writer.writeCodePoint(0xFEFF);
}
String omitXMLDeclaration = outputProperties.getProperty(OutputKeys.OMIT_XML_DECLARATION);
if (omitXMLDeclaration == null) {
omitXMLDeclaration = "no";
}
if (canonical) {
omitXMLDeclaration = "yes";
}
String version = outputProperties.getProperty(OutputKeys.VERSION);
if (version == null) {
version = getConfiguration().getXMLVersion() == Configuration.XML10 ? "1.0" : "1.1";
} else {
if (!version.equals("1.0") && !version.equals("1.1")) {
if (unfailing) {
version = "1.0";
} else {
XPathException err = new XPathException("XML version must be 1.0 or 1.1");
err.setErrorCode("SESU0013");
throw err;
}
}
if (!version.equals("1.0") && omitXMLDeclaration.equals("yes") &&
outputProperties.getProperty(OutputKeys.DOCTYPE_SYSTEM) != null) {
if (!unfailing) {
XPathException err = new XPathException("Values of 'version', 'omit-xml-declaration', and 'doctype-system' conflict");
err.setErrorCode("SEPM0009");
throw err;
}
}
}
String undeclare = outputProperties.getProperty(SaxonOutputKeys.UNDECLARE_PREFIXES);
if ("yes".equals(undeclare)) {
undeclareNamespaces = true;
}
if (version.equals("1.0") && undeclareNamespaces) {
if (unfailing) {
undeclareNamespaces = false;
} else {
XPathException err = new XPathException("Cannot undeclare namespaces with XML version 1.0");
err.setErrorCode("SEPM0010");
throw err;
}
}
String standalone = outputProperties.getProperty(OutputKeys.STANDALONE);
if ("omit".equals(standalone)) {
standalone = null;
}
if (standalone != null) {
requireWellFormed = true;
if (omitXMLDeclaration.equals("yes") && !unfailing) {
XPathException err = new XPathException("Values of 'standalone' and 'omit-xml-declaration' conflict");
err.setErrorCode("SEPM0009");
throw err;
}
}
String systemId = outputProperties.getProperty(OutputKeys.DOCTYPE_SYSTEM);
if (systemId != null && !"".equals(systemId)) {
requireWellFormed = true;
}
if (omitXMLDeclaration.equals("no")) {
writer.writeAscii(XML_DECL_VERSION);
writer.write(version);
writer.writeAscii(QUOTE_SPACE);
writer.writeAscii(XML_DECL_ENCODING);
writer.write(encoding);
writer.writeCodePoint('"');
if (standalone != null) {
writer.writeAscii(XML_DECL_STANDALONE);
writer.write(standalone);
writer.writeCodePoint('"');
}
writer.writeAscii(StringConstants.PI_END);
// writer.write("");
// don't write a newline character: it's wrong if the output is an
// external general parsed entity
}
} catch (java.io.IOException err) {
throw new XPathException("Failure writing to " + getSystemId(), err);
}
}
private static final byte[] XML_DECL_VERSION = StringConstants.bytes("\n");
/**
* Output the document type declaration
*
* @param name the qualified name of the element
* @param displayName The element name as displayed
* @param systemId The DOCTYPE system identifier
* @param publicId The DOCTYPE public identifier
* @throws net.sf.saxon.trans.XPathException if an error occurs writing to the output
*/
protected void writeDocType(NodeName name, String displayName, String systemId, String publicId) throws XPathException {
try {
if (!canonical) {
if (declarationIsWritten && !indenting) {
// don't add a newline if indenting, because the indenter will already have done so
writer.writeCodePoint(0x0A);
}
writer.writeAscii(DOCTYPE);
writer.write(displayName);
writer.writeCodePoint(0x0A);
String quotedSystemId = null;
if (systemId != null) {
if (systemId.contains("\"")) {
quotedSystemId = "'" + systemId + "'";
} else {
quotedSystemId = '"' + systemId + '"';
}
}
if (systemId != null && publicId == null) {
writer.writeAscii(SYSTEM);
writer.write(quotedSystemId);
} else if (systemId == null && publicId != null) { // handles the HTML case
writer.writeAscii(PUBLIC);
writer.write(publicId);
writer.writeCodePoint('"');
} else if (publicId != null) {
writer.writeAscii(PUBLIC);
writer.write(publicId);
writer.writeAscii(QUOTE_SPACE);
writer.write(quotedSystemId);
}
if (internalSubset != null) {
writer.writeCodePoint('[');
writer.writeCodePoint(0x0A);
writer.write(internalSubset);
writer.writeCodePoint(0x0A);
writer.writeCodePoint(']');
}
writer.writeAscii(RIGHT_ANGLE_NEWLINE);
}
} catch (java.io.IOException err) {
throw new XPathException("Failure writing to " + getSystemId(), err);
}
}
/**
* End of the document.
*/
@Override
public void close() throws XPathException {
// if nothing has been written, we should still create the file and write an XML declaration
if (!started) {
openDocument();
}
try {
if (writer != null) {
writer.flush();
}
} catch (java.io.IOException err) {
throw new XPathException("Failure writing to " + getSystemId(), err);
}
super.close();
}
/**
* Start of an element. Output the start tag, escaping special characters.
*
* @param elemName the name of the element
* @param type the type annotation of the element
* @param attributes the attributes of this element
* @param namespaces the in-scope namespaces of this element: generally this is all the in-scope
* namespaces, without relying on inheriting namespaces from parent elements
* @param location an object providing information about the module, line, and column where the node originated
* @param properties bit-significant properties of the element node. If there are no relevant
* properties, zero is supplied. The definitions of the bits are in class {@link ReceiverOption}
* @throws XPathException if an error occurs
*/
@Override
public void startElement(NodeName elemName, SchemaType type,
AttributeMap attributes, NamespaceMap namespaces,
Location location, int properties) throws XPathException {
previousAtomic = false;
if (!started) {
openDocument();
} else if (requireWellFormed && elementStack.isEmpty() && startedElement && !unfailing) {
XPathException err = new XPathException("When 'standalone' or 'doctype-system' is specified, " +
"the document must be well-formed; but this document contains more than one top-level element");
err.setErrorCode("SEPM0004");
throw err;
}
startedElement = true;
String displayName = elemName.getDisplayName();
if (!allCharactersEncodable) {
int badchar = testCharacters(StringView.of(displayName));
if (badchar != 0) {
XPathException err = new XPathException("Element name contains a character (decimal + " +
badchar + ") not available in the selected encoding");
err.setErrorCode("SERE0008");
throw err;
}
}
elementStack.push(displayName);
elementCode = elemName;
try {
if (!started) {
String systemId = outputProperties.getProperty(OutputKeys.DOCTYPE_SYSTEM);
String publicId = outputProperties.getProperty(OutputKeys.DOCTYPE_PUBLIC);
// Treat "" as equivalent to absent. This goes beyond what the spec strictly allows.
if ("".equals(systemId)) {
systemId = null;
}
if ("".equals(publicId)) {
publicId = null;
}
if (systemId != null) {
requireWellFormed = true;
writeDocType(elemName, displayName, systemId, publicId);
} else if (writeDocTypeWithNullSystemId()) {
writeDocType(elemName, displayName, null, publicId);
}
started = true;
}
if (openStartTag) {
closeStartTag();
}
writer.writeCodePoint('<');
writer.write(displayName);
if (indentForNextAttribute >= 0) {
indentForNextAttribute += displayName.length();
}
boolean isFirst = true;
for (NamespaceBinding ns : namespaces) {
namespace(ns.getPrefix(), ns.getURI(), isFirst);
isFirst = false;
}
for (AttributeInfo att : attributes) {
attribute(att.getNodeName(), att.getValue(), att.getProperties(), isFirst);
isFirst = false;
}
openStartTag = true;
indentForNextAttribute = -1;
} catch (java.io.IOException err) {
throw new XPathException("Failure writing to " + getSystemId(), err);
}
}
protected boolean writeDocTypeWithNullSystemId() {
return internalSubset != null;
}
public void namespace(String nsprefix, String nsuri, boolean isFirst) throws XPathException {
try {
if (nsprefix.isEmpty()) {
if (isFirst) {
writer.writeCodePoint(' ');
} else {
writeAttributeIndentString();
}
writeAttribute(elementCode, "xmlns", nsuri, ReceiverOption.NONE);
} else //noinspection StatementWithEmptyBody
if (nsprefix.equals("xml")) {
//return;
} else {
int badchar = testCharacters(StringView.of(nsprefix));
if (badchar != 0) {
XPathException err = new XPathException("Namespace prefix contains a character (decimal + " +
badchar + ") not available in the selected encoding");
err.setErrorCode("SERE0008");
throw err;
}
if (undeclareNamespaces || !nsuri.isEmpty()) {
if (isFirst) {
writer.writeCodePoint(' ');
} else {
writeAttributeIndentString();
}
writeAttribute(elementCode, "xmlns:" + nsprefix, nsuri, ReceiverOption.NONE);
}
}
} catch (java.io.IOException err) {
throw new XPathException("Failure writing to " + getSystemId(), err);
}
}
/**
* Set the indentation to be used for attributes (this excludes the length of the
* element name itself)
* @param indent the number of spaces to be output before each attribute (on a new line)
*/
public void setIndentForNextAttribute(int indent) {
indentForNextAttribute = indent;
}
private void attribute(NodeName nameCode, String value, int properties, boolean isFirst)
throws XPathException {
String displayName = nameCode.getDisplayName();
if (!allCharactersEncodable) {
int badchar = testCharacters(StringView.of(displayName));
if (badchar != 0) {
if (unfailing) {
displayName = convertToAscii(StringView.of(displayName)).toString();
} else {
XPathException err = new XPathException("Attribute name contains a character (decimal + " +
badchar + ") not available in the selected encoding");
err.setErrorCode("SERE0008");
throw err;
}
}
}
try {
if (isFirst) {
writer.writeCodePoint(' ');
} else {
writeAttributeIndentString();
}
} catch (java.io.IOException err) {
throw new XPathException("Failure writing to " + getSystemId(), err);
}
writeAttribute(
elementCode,
displayName,
value,
properties);
}
protected void writeAttributeIndentString() throws IOException {
if (indentForNextAttribute < 0) {
writer.writeCodePoint(' ');
} else {
writer.writeCodePoint('\n');
writer.writeRepeatedAscii((byte)0x20, indentForNextAttribute);
}
}
/**
* Mark the end of the start tag
*
* @throws XPathException if an IO exception occurs
*/
public void closeStartTag() throws XPathException {
try {
if (openStartTag) {
writer.writeCodePoint('>');
openStartTag = false;
}
} catch (java.io.IOException err) {
throw new XPathException("Failure writing to " + getSystemId(), err);
}
}
/**
* Close an empty element tag. (This is overridden in XHTMLEmitter).
* @param displayName the name of the empty element
* @param nameCode the fingerprint of the name of the empty element
* @throws IOException if an IO exception occurs
*/
protected void writeEmptyElementTagCloser(String displayName, NodeName nameCode) throws IOException {
if (canonical) {
writer.writeCodePoint('>');
writer.writeAscii(StringConstants.END_TAG_START);
writer.write(displayName);
writer.writeCodePoint('>');
} else {
writer.writeAscii(StringConstants.EMPTY_TAG_END);
}
}
/**
* Write attribute name=value pair.
*
* @param elCode The element name is not used in this version of the
* method, but is used in the HTML subclass.
* @param attname The attribute name, which has already been validated to ensure
* it can be written in this encoding
* @param value The value of the attribute
* @param properties Any special properties of the attribute
* @throws net.sf.saxon.trans.XPathException if an error occurs
*/
protected void writeAttribute(NodeName elCode, String attname, String value, int properties) throws XPathException {
try {
writer.write(attname);
if (ReceiverOption.contains(properties, ReceiverOption.NO_SPECIAL_CHARS)) {
writer.writeCodePoint('=');
writer.writeCodePoint(delimiter);
writer.write(value);
writer.writeCodePoint(delimiter);
} else if (ReceiverOption.contains(properties, ReceiverOption.USE_NULL_MARKERS)) {
// null (0) characters will be used before and after any section of
// the value generated from a character map
writer.writeCodePoint('=');
char delim = value.indexOf('"') >= 0 && value.indexOf('\'') < 0 ? '\'' : delimiter;
writer.writeCodePoint(delim);
writeEscape(StringView.tidy(value), true);
writer.writeCodePoint(delim);
} else {
writer.writeCodePoint('=');
writer.writeCodePoint(delimiter);
if (ReceiverOption.contains(properties, ReceiverOption.DISABLE_ESCAPING)) {
writer.write(value);
} else {
writeEscape(StringView.tidy(value), true);
}
writer.writeCodePoint(delimiter);
}
} catch (java.io.IOException err) {
throw new XPathException("Failure writing to " + getSystemId(), err);
}
}
/**
* Test that all characters in a name (for example) are supported in the target encoding.
*
* @param chars the characters to be tested
* @return zero if all the characters are available, or the value of the
* first offending character if not
*/
protected int testCharacters(UnicodeString chars) {
IntIterator iter = chars.codePoints();
while (iter.hasNext()) {
int ch = iter.next();
if (ch > 127 && !characterSet.inCharset(ch)) {
return ch;
}
}
return 0;
}
/**
* Where characters are not available in the selected encoding, substitute them
*
* @param chars the characters to be converted
* @return the converted string
*/
protected UnicodeString convertToAscii(UnicodeString chars) {
UnicodeBuilder buff = new UnicodeBuilder();
IntIterator iter = chars.codePoints();
while (iter.hasNext()) {
int c = iter.next();
if (c >= 20 && c < 127) {
buff.append(c);
} else {
buff.append("_" + c + "_");
}
}
return buff.toUnicodeString();
}
/**
* End of an element.
*/
@Override
public void endElement() throws XPathException {
String displayName = elementStack.pop();
try {
if (openStartTag) {
writeEmptyElementTagCloser(displayName, elementCode);
openStartTag = false;
} else {
writer.writeAscii(StringConstants.END_TAG_START);
writer.write(displayName);
writer.writeCodePoint('>');
}
} catch (java.io.IOException err) {
throw new XPathException("Failure writing to " + getSystemId(), err);
}
}
/**
* Character data.
*/
@Override
public void characters(UnicodeString chars, Location locationId, int properties) throws XPathException {
if (!started) {
openDocument();
}
if (requireWellFormed && elementStack.isEmpty() && !Whitespace.isAllWhite(chars) && !unfailing) {
XPathException err = new XPathException("When 'standalone' or 'doctype-system' is specified, " +
"the document must be well-formed; but this document contains a top-level text node");
err.setErrorCode("SEPM0004");
throw err;
}
try {
if (openStartTag) {
closeStartTag();
}
if (chars instanceof WhitespaceString) {
((WhitespaceString) chars).write(writer);
} else if (ReceiverOption.contains(properties, ReceiverOption.NO_SPECIAL_CHARS)) {
writer.write(chars);
} else if (!ReceiverOption.contains(properties, ReceiverOption.DISABLE_ESCAPING)) {
writeEscape(chars, false);
} else {
// disable-output-escaping="yes"
if (testCharacters(chars) == 0) {
if (!ReceiverOption.contains(properties, ReceiverOption.USE_NULL_MARKERS)) {
// null (0) characters will be used before and after any section of
// the value generated from a character map
writer.write(chars);
} else {
// Need to strip out any null markers. See test output-html109
IntIterator iter = chars.codePoints();
while (iter.hasNext()) {
int c = iter.next();
if (c != 0) {
writer.writeCodePoint(c);
}
}
}
} else {
// Using disable output escaping with characters
// that are not available in the target encoding
// The required action is to ignore d-o-e in respect of those characters that are
// not available in the encoding. This is slow...
IntIterator iter = chars.codePoints();
while (iter.hasNext()) {
int c = iter.next();
if (c != 0) {
if (characterSet.inCharset(c)) {
writer.writeCodePoint(c);
} else {
writeEscape(new UnicodeChar(c), false);
}
}
}
}
}
} catch (java.io.IOException err) {
throw new XPathException("Failure writing to " + getSystemId(), err);
}
}
/**
* Handle a processing instruction.
*/
@Override
public void processingInstruction(String target, UnicodeString data, Location locationId, int properties)
throws XPathException {
if (!started) {
openDocument();
}
int x = testCharacters(StringView.of(target));
if (x != 0) {
if (unfailing) {
target = convertToAscii(StringView.of(target)).toString();
} else {
XPathException err = new XPathException("Character in processing instruction name cannot be represented " +
"in the selected encoding (code " + x + ')');
err.setErrorCode("SERE0008");
throw err;
}
}
x = testCharacters(data);
if (x != 0) {
if (unfailing) {
data = convertToAscii(data);
} else {
XPathException err = new XPathException("Character in processing instruction data cannot be represented " +
"in the selected encoding (code " + x + ')');
err.setErrorCode("SERE0008");
throw err;
}
}
try {
if (openStartTag) {
closeStartTag();
}
writer.writeAscii(StringConstants.PI_START);
writer.write(target);
if (!data.isEmpty()) {
writer.writeCodePoint(0x20);
writer.write(data);
}
writer.writeAscii(StringConstants.PI_END);
} catch (java.io.IOException err) {
throw new XPathException("Failure writing to " + getSystemId(), err);
}
}
/**
* Write contents of array to current writer, after escaping special characters.
* This method converts the XML special characters (such as < and &) into their
* predefined entities.
* @param chars The character sequence containing the string
* @param inAttribute Set to true if the text is in an attribute value
* @throws IOException if an IO exception occurs
* @throws XPathException if an IO exception occurs
*/
protected void writeEscape(UnicodeString chars, final boolean inAttribute)
throws java.io.IOException, XPathException {
long segstart = 0;
boolean disabled = false;
final boolean[] specialChars = inAttribute ? attSpecials : specialInText;
if (chars instanceof WhitespaceString) {
((WhitespaceString) chars).writeEscape(specialChars, writer);
return;
}
IntPredicate special = inAttribute ? isSpecialInAttribute : isSpecialInText;
final long clength = chars.length();
while (segstart < clength) {
// find a maximal sequence of "ordinary" characters
long found = chars.indexWhere(special, segstart);
long i = found == -1 ? clength : found;
// if this was the whole (or remainder of the) string write it out and exit
if (found < 0) {
if (segstart == 0) {
writer.write(chars);
} else {
writer.write(chars.substring(segstart, clength));
}
return;
}
// otherwise write out this sequence
if (i > segstart) {
writer.write(chars.substring(segstart, i));
}
// examine the special character that interrupted the scan
final int c = chars.codePointAt(i);
if (c == 0) {
// used to switch escaping on and off
disabled = !disabled;
} else if (disabled) {
if (c > 127 && !characterSet.inCharset(c)) {
XPathException de = new XPathException("Character " + c + " (x" + Integer.toHexString(c) +
") is not available in the chosen encoding");
de.setErrorCode("SERE0008");
throw de;
}
writeCodePoint(c);
} else if (c < 127) {
// process special ASCII characters
switch (c) {
case '<':
writer.writeAscii(StringConstants.ESCAPE_LT);
break;
case '>':
writer.writeAscii(StringConstants.ESCAPE_GT);
break;
case '&':
writer.writeAscii(StringConstants.ESCAPE_AMP);
break;
case '\"':
writer.writeAscii(StringConstants.ESCAPE_QUOT);
break;
case '\'':
writer.writeAscii(StringConstants.ESCAPE_APOS);
break;
case '\n':
writer.writeAscii(StringConstants.ESCAPE_NL);
break;
case '\r':
writer.writeAscii(StringConstants.ESCAPE_CR);
break;
case '\t':
writer.writeAscii(StringConstants.ESCAPE_TAB);
break;
default:
// C0 control characters
characterReferenceGenerator.outputCharacterReference(c, writer);
break;
}
} else if (c < 160 || c == 0x2028) {
// XML 1.1 requires these characters to be written as character references
characterReferenceGenerator.outputCharacterReference(c, writer);
} else if (c > 65535) {
if (characterSet.inCharset(c)) {
writeCodePoint(c);
} else {
characterReferenceGenerator.outputCharacterReference(c, writer);
}
} else {
// process characters not available in the current encoding
characterReferenceGenerator.outputCharacterReference(c, writer);
}
segstart = ++i;
}
}
protected void writeCodePoint(int c) throws IOException {
writer.writeCodePoint(c);
}
/**
* Handle a comment.
*/
@Override
public void comment(UnicodeString chars, Location locationId, int properties) throws XPathException {
if (!started) {
openDocument();
}
int x = testCharacters(chars);
if (x != 0) {
if (unfailing) {
chars = convertToAscii(chars);
} else {
XPathException err = new XPathException("Character in comment cannot be represented " +
"in the selected encoding (code " + x + ')');
err.setErrorCode("SERE0008");
throw err;
}
}
try {
if (openStartTag) {
closeStartTag();
}
writer.writeAscii(StringConstants.COMMENT_START);
writer.write(chars);
writer.writeAscii(StringConstants.COMMENT_END);
} catch (java.io.IOException err) {
throw new XPathException("Failure writing to " + getSystemId(), err);
}
}
/**
* 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
*/
@Override
public boolean usesTypeAnnotations() {
return false;
}
/**
* Ask whether anything has yet been written
*
* @return true if content has been output
*/
public boolean isStarted() {
return started;
}
}