XMLWriter.java - serialize an XML document
// XMLWriter.java - serialize an XML document.
// Written by David Megginson, david@megginson.com
// and placed by him into the public domain.
// Extensively modified by John Cowan for TagSoup.
// TagSoup is licensed under the Apache License,
// Version 2.0. You may obtain a copy of this license at
// http://www.apache.org/licenses/LICENSE-2.0 . You may also have
// additional legal rights not granted by this license.
//
// TagSoup is distributed in the hope that it will be useful, but
// unless required by applicable law or agreed to in writing, TagSoup
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
// OF ANY KIND, either express or implied; not even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Properties;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.AttributesImpl;
import org.xml.sax.helpers.NamespaceSupport;
import org.xml.sax.helpers.XMLFilterImpl;
import org.xml.sax.ext.LexicalHandler;
/**
* Filter to write an XML document from a SAX event stream.
*
* <p>This class can be used by itself or as part of a SAX event
* stream: it takes as input a series of SAX2 ContentHandler
* events and uses the information in those events to write
* an XML document. Since this class is a filter, it can also
* pass the events on down a filter chain for further processing
* (you can use the XMLWriter to take a snapshot of the current
* state at any point in a filter chain), and it can be
* used directly as a ContentHandler for a SAX2 XMLReader.</p>
*
* <p>The client creates a document by invoking the methods for
* standard SAX2 events, always beginning with the
* {@link #startDocument startDocument} method and ending with
* the {@link #endDocument endDocument} method. There are convenience
* methods provided so that clients to not have to create empty
* attribute lists or provide empty strings as parameters; for
* example, the method invocation</p>
*
* <pre>
* w.startElement("foo");
* </pre>
*
* <p>is equivalent to the regular SAX2 ContentHandler method</p>
*
* <pre>
* w.startElement("", "foo", "", new AttributesImpl());
* </pre>
*
* <p>Except that it is more efficient because it does not allocate
* a new empty attribute list each time. The following code will send
* a simple XML document to standard output:</p>
*
* <pre>
* XMLWriter w = new XMLWriter();
*
* w.startDocument();
* w.startElement("greeting");
* w.characters("Hello, world!");
* w.endElement("greeting");
* w.endDocument();
* </pre>
*
* <p>The resulting document will look like this:</p>
*
* <pre>
* <?xml version="1.0" standalone="yes"?>
*
* <greeting>Hello, world!</greeting>
* </pre>
*
* <p>In fact, there is an even simpler convenience method,
* <var>dataElement</var>, designed for writing elements that
* contain only character data, so the code to generate the
* document could be shortened to</p>
*
* <pre>
* XMLWriter w = new XMLWriter();
*
* w.startDocument();
* w.dataElement("greeting", "Hello, world!");
* w.endDocument();
* </pre>
*
* <h2>Whitespace</h2>
*
* <p>According to the XML Recommendation, <em>all</em> whitespace
* in an XML document is potentially significant to an application,
* so this class never adds newlines or indentation. If you
* insert three elements in a row, as in</p>
*
* <pre>
* w.dataElement("item", "1");
* w.dataElement("item", "2");
* w.dataElement("item", "3");
* </pre>
*
* <p>you will end up with</p>
*
* <pre>
* <item>1</item><item>3</item><item>3</item>
* </pre>
*
* <p>You need to invoke one of the <var>characters</var> methods
* explicitly to add newlines or indentation. Alternatively, you
* can use {@link com.megginson.sax.DataWriter DataWriter}, which
* is derived from this class -- it is optimized for writing
* purely data-oriented (or field-oriented) XML, and does automatic
* linebreaks and indentation (but does not support mixed content
* properly).</p>
*
*
* <h2>Namespace Support</h2>
*
* <p>The writer contains extensive support for XML Namespaces, so that
* a client application does not have to keep track of prefixes and
* supply <var>xmlns</var> attributes. By default, the XML writer will
* generate Namespace declarations in the form _NS1, _NS2, etc., wherever
* they are needed, as in the following example:</p>
*
* <pre>
* w.startDocument();
* w.emptyElement("http://www.foo.com/ns/", "foo");
* w.endDocument();
* </pre>
*
* <p>The resulting document will look like this:</p>
*
* <pre>
* <?xml version="1.0" standalone="yes"?>
*
* <_NS1:foo xmlns:_NS1="http://www.foo.com/ns/"/>
* </pre>
*
* <p>In many cases, document authors will prefer to choose their
* own prefixes rather than using the (ugly) default names. The
* XML writer allows two methods for selecting prefixes:</p>
*
* <ol>
* <li>the qualified name</li>
* <li>the {@link #setPrefix setPrefix} method.</li>
* </ol>
*
* <p>Whenever the XML writer finds a new Namespace URI, it checks
* to see if a qualified (prefixed) name is also available; if so
* it attempts to use the name's prefix (as long as the prefix is
* not already in use for another Namespace URI).</p>
*
* <p>Before writing a document, the client can also pre-map a prefix
* to a Namespace URI with the setPrefix method:</p>
*
* <pre>
* w.setPrefix("http://www.foo.com/ns/", "foo");
* w.startDocument();
* w.emptyElement("http://www.foo.com/ns/", "foo");
* w.endDocument();
* </pre>
*
* <p>The resulting document will look like this:</p>
*
* <pre>
* <?xml version="1.0" standalone="yes"?>
*
* <foo:foo xmlns:foo="http://www.foo.com/ns/"/>
* </pre>
*
* <p>The default Namespace simply uses an empty string as the prefix:</p>
*
* <pre>
* w.setPrefix("http://www.foo.com/ns/", "");
* w.startDocument();
* w.emptyElement("http://www.foo.com/ns/", "foo");
* w.endDocument();
* </pre>
*
* <p>The resulting document will look like this:</p>
*
* <pre>
* <?xml version="1.0" standalone="yes"?>
*
* <foo xmlns="http://www.foo.com/ns/"/>
* </pre>
*
* <p>By default, the XML writer will not declare a Namespace until
* it is actually used. Sometimes, this approach will create
* a large number of Namespace declarations, as in the following
* example:</p>
*
* <pre>
* <xml version="1.0" standalone="yes"?>
*
* <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
* <rdf:Description about="http://www.foo.com/ids/books/12345">
* <dc:title xmlns:dc="http://www.purl.org/dc/">A Dark Night</dc:title>
* <dc:creator xmlns:dc="http://www.purl.org/dc/">Jane Smith</dc:title>
* <dc:date xmlns:dc="http://www.purl.org/dc/">2000-09-09</dc:title>
* </rdf:Description>
* </rdf:RDF>
* </pre>
*
* <p>The "rdf" prefix is declared only once, because the RDF Namespace
* is used by the root element and can be inherited by all of its
* descendants; the "dc" prefix, on the other hand, is declared three
* times, because no higher element uses the Namespace. To solve this
* problem, you can instruct the XML writer to predeclare Namespaces
* on the root element even if they are not used there:</p>
*
* <pre>
* w.forceNSDecl("http://www.purl.org/dc/");
* </pre>
*
* <p>Now, the "dc" prefix will be declared on the root element even
* though it's not needed there, and can be inherited by its
* descendants:</p>
*
* <pre>
* <xml version="1.0" standalone="yes"?>
*
* <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
* xmlns:dc="http://www.purl.org/dc/">
* <rdf:Description about="http://www.foo.com/ids/books/12345">
* <dc:title>A Dark Night</dc:title>
* <dc:creator>Jane Smith</dc:title>
* <dc:date>2000-09-09</dc:title>
* </rdf:Description>
* </rdf:RDF>
* </pre>
*
* <p>This approach is also useful for declaring Namespace prefixes
* that be used by qualified names appearing in attribute values or
* character data.</p>
*
* @author David Megginson, david@megginson.com
* @version 0.2
* @see org.xml.sax.XMLFilter
* @see org.xml.sax.ContentHandler
*/
public class XMLWriter extends XMLFilterImpl implements LexicalHandler
{
////////////////////////////////////////////////////////////////////
// Constructors.
////////////////////////////////////////////////////////////////////
/**
* Create a new XML writer.
*
* <p>Write to standard output.</p>
*/
public XMLWriter ()
{
init(null);
}
/**
* Create a new XML writer.
*
* <p>Write to the writer provided.</p>
*
* @param writer The output destination, or null to use standard
* output.
*/
public XMLWriter (Writer writer)
{
init(writer);
}
/**
* Create a new XML writer.
*
* <p>Use the specified XML reader as the parent.</p>
*
* @param xmlreader The parent in the filter chain, or null
* for no parent.
*/
public XMLWriter (XMLReader xmlreader)
{
super(xmlreader);
init(null);
}
/**
* Create a new XML writer.
*
* <p>Use the specified XML reader as the parent, and write
* to the specified writer.</p>
*
* @param xmlreader The parent in the filter chain, or null
* for no parent.
* @param writer The output destination, or null to use standard
* output.
*/
public XMLWriter (XMLReader xmlreader, Writer writer)
{
super(xmlreader);
init(writer);
}
/**
* Internal initialization method.
*
* <p>All of the public constructors invoke this method.
*
* @param writer The output destination, or null to use
* standard output.
*/
private void init (Writer writer)
{
setOutput(writer);
nsSupport = new NamespaceSupport();
prefixTable = new Hashtable();
forcedDeclTable = new Hashtable();
doneDeclTable = new Hashtable();
outputProperties = new Properties();
}
////////////////////////////////////////////////////////////////////
// Public methods.
////////////////////////////////////////////////////////////////////
/**
* Reset the writer.
*
* <p>This method is especially useful if the writer throws an
* exception before it is finished, and you want to reuse the
* writer for a new document. It is usually a good idea to
* invoke {@link #flush flush} before resetting the writer,
* to make sure that no output is lost.</p>
*
* <p>This method is invoked automatically by the
* {@link #startDocument startDocument} method before writing
* a new document.</p>
*
* <p><strong>Note:</strong> this method will <em>not</em>
* clear the prefix or URI information in the writer or
* the selected output writer.</p>
*
* @see #flush
*/
public void reset ()
{
elementLevel = 0;
prefixCounter = 0;
nsSupport.reset();
}
/**
* Flush the output.
*
* <p>This method flushes the output stream. It is especially useful
* when you need to make certain that the entire document has
* been written to output but do not want to close the output
* stream.</p>
*
* <p>This method is invoked automatically by the
* {@link #endDocument endDocument} method after writing a
* document.</p>
*
* @see #reset
*/
public void flush ()
throws IOException
{
output.flush();
}
/**
* Set a new output destination for the document.
*
* @param writer The output destination, or null to use
* standard output.
* @return The current output writer.
* @see #flush
*/
public void setOutput (Writer writer)
{
if (writer == null) {
output = new OutputStreamWriter(System.out);
} else {
output = writer;
}
}
/**
* Specify a preferred prefix for a Namespace URI.
*
* <p>Note that this method does not actually force the Namespace
* to be declared; to do that, use the {@link
* #forceNSDecl(java.lang.String) forceNSDecl} method as well.</p>
*
* @param uri The Namespace URI.
* @param prefix The preferred prefix, or "" to select
* the default Namespace.
* @see #getPrefix
* @see #forceNSDecl(java.lang.String)
* @see #forceNSDecl(java.lang.String,java.lang.String)
*/
public void setPrefix (String uri, String prefix)
{
prefixTable.put(uri, prefix);
}
/**
* Get the current or preferred prefix for a Namespace URI.
*
* @param uri The Namespace URI.
* @return The preferred prefix, or "" for the default Namespace.
* @see #setPrefix
*/
public String getPrefix (String uri)
{
return (String)prefixTable.get(uri);
}
/**
* Force a Namespace to be declared on the root element.
*
* <p>By default, the XMLWriter will declare only the Namespaces
* needed for an element; as a result, a Namespace may be
* declared many places in a document if it is not used on the
* root element.</p>
*
* <p>This method forces a Namespace to be declared on the root
* element even if it is not used there, and reduces the number
* of xmlns attributes in the document.</p>
*
* @param uri The Namespace URI to declare.
* @see #forceNSDecl(java.lang.String,java.lang.String)
* @see #setPrefix
*/
public void forceNSDecl (String uri)
{
forcedDeclTable.put(uri, Boolean.TRUE);
}
/**
* Force a Namespace declaration with a preferred prefix.
*
* <p>This is a convenience method that invokes {@link
* #setPrefix setPrefix} then {@link #forceNSDecl(java.lang.String)
* forceNSDecl}.</p>
*
* @param uri The Namespace URI to declare on the root element.
* @param prefix The preferred prefix for the Namespace, or ""
* for the default Namespace.
* @see #setPrefix
* @see #forceNSDecl(java.lang.String)
*/
public void forceNSDecl (String uri, String prefix)
{
setPrefix(uri, prefix);
forceNSDecl(uri);
}
////////////////////////////////////////////////////////////////////
// Methods from org.xml.sax.ContentHandler.
////////////////////////////////////////////////////////////////////
/**
* Write the XML declaration at the beginning of the document.
*
* Pass the event on down the filter chain for further processing.
*
* @exception org.xml.sax.SAXException If there is an error
* writing the XML declaration, or if a handler further down
* the filter chain raises an exception.
* @see org.xml.sax.ContentHandler#startDocument
*/
public void startDocument ()
throws SAXException
{
reset();
if (!("yes".equals(outputProperties.getProperty(OMIT_XML_DECLARATION, "no")))) {
write("<?xml");
if (version == null) {
write(" version=\"1.0\"");
} else {
write(" version=\"");
write(version);
write("\"");
}
if (outputEncoding != null && outputEncoding != "") {
write(" encoding=\"");
write(outputEncoding);
write("\"");
}
if (standalone == null) {
write(" standalone=\"yes\"?>\n");
} else {
write(" standalone=\"");
write(standalone);
write("\"");
}
}
super.startDocument();
}
/**
* Write a newline at the end of the document.
*
* Pass the event on down the filter chain for further processing.
*
* @exception org.xml.sax.SAXException If there is an error
* writing the newline, or if a handler further down
* the filter chain raises an exception.
* @see org.xml.sax.ContentHandler#endDocument
*/
public void endDocument ()
throws SAXException
{
write('\n');
super.endDocument();
try {
flush();
} catch (IOException e) {
throw new SAXException(e);
}
}
/**
* Write a start tag.
*
* Pass the event on down the filter chain for further processing.
*
* @param uri The Namespace URI, or the empty string if none
* is available.
* @param localName The element's local (unprefixed) name (required).
* @param qName The element's qualified (prefixed) name, or the
* empty string is none is available. This method will
* use the qName as a template for generating a prefix
* if necessary, but it is not guaranteed to use the
* same qName.
* @param atts The element's attribute list (must not be null).
* @exception org.xml.sax.SAXException If there is an error
* writing the start tag, or if a handler further down
* the filter chain raises an exception.
* @see org.xml.sax.ContentHandler#startElement
*/
public void startElement (String uri, String localName,
String qName, Attributes atts)
throws SAXException
{
elementLevel++;
nsSupport.pushContext();
if (forceDTD && !hasOutputDTD) startDTD(localName == null ? qName : localName, "", "");
write('<');
writeName(uri, localName, qName, true);
writeAttributes(atts);
if (elementLevel == 1) {
forceNSDecls();
}
writeNSDecls();
write('>');
// System.out.println("%%%% startElement [" + qName + "] htmlMode = " + htmlMode);
if (htmlMode && (qName.equals("script") || qName.equals("style"))) {
cdataElement = true;
// System.out.println("%%%% CDATA element");
}
super.startElement(uri, localName, qName, atts);
}
/**
* Write an end tag.
*
* Pass the event on down the filter chain for further processing.
*
* @param uri The Namespace URI, or the empty string if none
* is available.
* @param localName The element's local (unprefixed) name (required).
* @param qName The element's qualified (prefixed) name, or the
* empty string is none is available. This method will
* use the qName as a template for generating a prefix
* if necessary, but it is not guaranteed to use the
* same qName.
* @exception org.xml.sax.SAXException If there is an error
* writing the end tag, or if a handler further down
* the filter chain raises an exception.
* @see org.xml.sax.ContentHandler#endElement
*/
public void endElement (String uri, String localName, String qName)
throws SAXException
{
if (!(htmlMode &&
(uri.equals("http://www.w3.org/1999/xhtml") ||
uri.equals("")) &&
(qName.equals("area") || qName.equals("base") ||
qName.equals("basefont") || qName.equals("br") ||
qName.equals("col") || qName.equals("frame") ||
qName.equals("hr") || qName.equals("img") ||
qName.equals("input") || qName.equals("isindex") ||
qName.equals("link") || qName.equals("meta") ||
qName.equals("param")))) {
write("</");
writeName(uri, localName, qName, true);
write('>');
}
if (elementLevel == 1) {
write('\n');
}
cdataElement = false;
super.endElement(uri, localName, qName);
nsSupport.popContext();
elementLevel--;
}
/**
* Write character data.
*
* Pass the event on down the filter chain for further processing.
*
* @param ch The array of characters to write.
* @param start The starting position in the array.
* @param length The number of characters to write.
* @exception org.xml.sax.SAXException If there is an error
* writing the characters, or if a handler further down
* the filter chain raises an exception.
* @see org.xml.sax.ContentHandler#characters
*/
public void characters (char ch[], int start, int len)
throws SAXException
{
if (!cdataElement) {
writeEsc(ch, start, len, false);
}
else {
for (int i = start; i < start + len; i++) {
write(ch[i]);
}
}
super.characters(ch, start, len);
}
/**
* Write ignorable whitespace.
*
* Pass the event on down the filter chain for further processing.
*
* @param ch The array of characters to write.
* @param start The starting position in the array.
* @param length The number of characters to write.
* @exception org.xml.sax.SAXException If there is an error
* writing the whitespace, or if a handler further down
* the filter chain raises an exception.
* @see org.xml.sax.ContentHandler#ignorableWhitespace
*/
public void ignorableWhitespace (char ch[], int start, int length)
throws SAXException
{
writeEsc(ch, start, length, false);
super.ignorableWhitespace(ch, start, length);
}
/**
* Write a processing instruction.
*
* Pass the event on down the filter chain for further processing.
*
* @param target The PI target.
* @param data The PI data.
* @exception org.xml.sax.SAXException If there is an error
* writing the PI, or if a handler further down
* the filter chain raises an exception.
* @see org.xml.sax.ContentHandler#processingInstruction
*/
public void processingInstruction (String target, String data)
throws SAXException
{
write("<?");
write(target);
write(' ');
write(data);
write("?>");
if (elementLevel < 1) {
write('\n');
}
super.processingInstruction(target, data);
}
////////////////////////////////////////////////////////////////////
// Additional markup.
////////////////////////////////////////////////////////////////////
/**
* Write an empty element.
*
* This method writes an empty element tag rather than a start tag
* followed by an end tag. Both a {@link #startElement
* startElement} and an {@link #endElement endElement} event will
* be passed on down the filter chain.
*
* @param uri The element's Namespace URI, or the empty string
* if the element has no Namespace or if Namespace
* processing is not being performed.
* @param localName The element's local name (without prefix). This
* parameter must be provided.
* @param qName The element's qualified name (with prefix), or
* the empty string if none is available. This parameter
* is strictly advisory: the writer may or may not use
* the prefix attached.
* @param atts The element's attribute list.
* @exception org.xml.sax.SAXException If there is an error
* writing the empty tag, or if a handler further down
* the filter chain raises an exception.
* @see #startElement
* @see #endElement
*/
public void emptyElement (String uri, String localName,
String qName, Attributes atts)
throws SAXException
{
nsSupport.pushContext();
write('<');
writeName(uri, localName, qName, true);
writeAttributes(atts);
if (elementLevel == 1) {
forceNSDecls();
}
writeNSDecls();
write("/>");
super.startElement(uri, localName, qName, atts);
super.endElement(uri, localName, qName);
}
////////////////////////////////////////////////////////////////////
// Convenience methods.
////////////////////////////////////////////////////////////////////
/**
* Start a new element without a qname or attributes.
*
* <p>This method will provide a default empty attribute
* list and an empty string for the qualified name.
* It invokes {@link
* #startElement(String, String, String, Attributes)}
* directly.</p>
*
* @param uri The element's Namespace URI.
* @param localName The element's local name.
* @exception org.xml.sax.SAXException If there is an error
* writing the start tag, or if a handler further down
* the filter chain raises an exception.
* @see #startElement(String, String, String, Attributes)
*/
public void startElement (String uri, String localName)
throws SAXException
{
startElement(uri, localName, "", EMPTY_ATTS);
}
/**
* Start a new element without a qname, attributes or a Namespace URI.
*
* <p>This method will provide an empty string for the
* Namespace URI, and empty string for the qualified name,
* and a default empty attribute list. It invokes
* #startElement(String, String, String, Attributes)}
* directly.</p>
*
* @param localName The element's local name.
* @exception org.xml.sax.SAXException If there is an error
* writing the start tag, or if a handler further down
* the filter chain raises an exception.
* @see #startElement(String, String, String, Attributes)
*/
public void startElement (String localName)
throws SAXException
{
startElement("", localName, "", EMPTY_ATTS);
}
/**
* End an element without a qname.
*
* <p>This method will supply an empty string for the qName.
* It invokes {@link #endElement(String, String, String)}
* directly.</p>
*
* @param uri The element's Namespace URI.
* @param localName The element's local name.
* @exception org.xml.sax.SAXException If there is an error
* writing the end tag, or if a handler further down
* the filter chain raises an exception.
* @see #endElement(String, String, String)
*/
public void endElement (String uri, String localName)
throws SAXException
{
endElement(uri, localName, "");
}
/**
* End an element without a Namespace URI or qname.
*
* <p>This method will supply an empty string for the qName
* and an empty string for the Namespace URI.
* It invokes {@link #endElement(String, String, String)}
* directly.</p>
*
* @param localName The element's local name.
* @exception org.xml.sax.SAXException If there is an error
* writing the end tag, or if a handler further down
* the filter chain raises an exception.
* @see #endElement(String, String, String)
*/
public void endElement (String localName)
throws SAXException
{
endElement("", localName, "");
}
/**
* Add an empty element without a qname or attributes.
*
* <p>This method will supply an empty string for the qname
* and an empty attribute list. It invokes
* {@link #emptyElement(String, String, String, Attributes)}
* directly.</p>
*
* @param uri The element's Namespace URI.
* @param localName The element's local name.
* @exception org.xml.sax.SAXException If there is an error
* writing the empty tag, or if a handler further down
* the filter chain raises an exception.
* @see #emptyElement(String, String, String, Attributes)
*/
public void emptyElement (String uri, String localName)
throws SAXException
{
emptyElement(uri, localName, "", EMPTY_ATTS);
}
/**
* Add an empty element without a Namespace URI, qname or attributes.
*
* <p>This method will supply an empty string for the qname,
* and empty string for the Namespace URI, and an empty
* attribute list. It invokes
* {@link #emptyElement(String, String, String, Attributes)}
* directly.</p>
*
* @param localName The element's local name.
* @exception org.xml.sax.SAXException If there is an error
* writing the empty tag, or if a handler further down
* the filter chain raises an exception.
* @see #emptyElement(String, String, String, Attributes)
*/
public void emptyElement (String localName)
throws SAXException
{
emptyElement("", localName, "", EMPTY_ATTS);
}
/**
* Write an element with character data content.
*
* <p>This is a convenience method to write a complete element
* with character data content, including the start tag
* and end tag.</p>
*
* <p>This method invokes
* {@link #startElement(String, String, String, Attributes)},
* followed by
* {@link #characters(String)}, followed by
* {@link #endElement(String, String, String)}.</p>
*
* @param uri The element's Namespace URI.
* @param localName The element's local name.
* @param qName The element's default qualified name.
* @param atts The element's attributes.
* @param content The character data content.
* @exception org.xml.sax.SAXException If there is an error
* writing the empty tag, or if a handler further down
* the filter chain raises an exception.
* @see #startElement(String, String, String, Attributes)
* @see #characters(String)
* @see #endElement(String, String, String)
*/
public void dataElement (String uri, String localName,
String qName, Attributes atts,
String content)
throws SAXException
{
startElement(uri, localName, qName, atts);
characters(content);
endElement(uri, localName, qName);
}
/**
* Write an element with character data content but no attributes.
*
* <p>This is a convenience method to write a complete element
* with character data content, including the start tag
* and end tag. This method provides an empty string
* for the qname and an empty attribute list.</p>
*
* <p>This method invokes
* {@link #startElement(String, String, String, Attributes)},
* followed by
* {@link #characters(String)}, followed by
* {@link #endElement(String, String, String)}.</p>
*
* @param uri The element's Namespace URI.
* @param localName The element's local name.
* @param content The character data content.
* @exception org.xml.sax.SAXException If there is an error
* writing the empty tag, or if a handler further down
* the filter chain raises an exception.
* @see #startElement(String, String, String, Attributes)
* @see #characters(String)
* @see #endElement(String, String, String)
*/
public void dataElement (String uri, String localName, String content)
throws SAXException
{
dataElement(uri, localName, "", EMPTY_ATTS, content);
}
/**
* Write an element with character data content but no attributes or Namespace URI.
*
* <p>This is a convenience method to write a complete element
* with character data content, including the start tag
* and end tag. The method provides an empty string for the
* Namespace URI, and empty string for the qualified name,
* and an empty attribute list.</p>
*
* <p>This method invokes
* {@link #startElement(String, String, String, Attributes)},
* followed by
* {@link #characters(String)}, followed by
* {@link #endElement(String, String, String)}.</p>
*
* @param localName The element's local name.
* @param content The character data content.
* @exception org.xml.sax.SAXException If there is an error
* writing the empty tag, or if a handler further down
* the filter chain raises an exception.
* @see #startElement(String, String, String, Attributes)
* @see #characters(String)
* @see #endElement(String, String, String)
*/
public void dataElement (String localName, String content)
throws SAXException
{
dataElement("", localName, "", EMPTY_ATTS, content);
}
/**
* Write a string of character data, with XML escaping.
*
* <p>This is a convenience method that takes an XML
* String, converts it to a character array, then invokes
* {@link #characters(char[], int, int)}.</p>
*
* @param data The character data.
* @exception org.xml.sax.SAXException If there is an error
* writing the string, or if a handler further down
* the filter chain raises an exception.
* @see #characters(char[], int, int)
*/
public void characters (String data)
throws SAXException
{
char ch[] = data.toCharArray();
characters(ch, 0, ch.length);
}
////////////////////////////////////////////////////////////////////
// Internal methods.
////////////////////////////////////////////////////////////////////
/**
* Force all Namespaces to be declared.
*
* This method is used on the root element to ensure that
* the predeclared Namespaces all appear.
*/
private void forceNSDecls ()
{
Enumeration prefixes = forcedDeclTable.keys();
while (prefixes.hasMoreElements()) {
String prefix = (String)prefixes.nextElement();
doPrefix(prefix, null, true);
}
}
/**
* Determine the prefix for an element or attribute name.
*
* TODO: this method probably needs some cleanup.
*
* @param uri The Namespace URI.
* @param qName The qualified name (optional); this will be used
* to indicate the preferred prefix if none is currently
* bound.
* @param isElement true if this is an element name, false
* if it is an attribute name (which cannot use the
* default Namespace).
*/
private String doPrefix (String uri, String qName, boolean isElement)
{
String defaultNS = nsSupport.getURI("");
if ("".equals(uri)) {
if (isElement && defaultNS != null)
nsSupport.declarePrefix("", "");
return null;
}
String prefix;
if (isElement && defaultNS != null && uri.equals(defaultNS)) {
prefix = "";
} else {
prefix = nsSupport.getPrefix(uri);
}
if (prefix != null) {
return prefix;
}
prefix = (String) doneDeclTable.get(uri);
if (prefix != null &&
((!isElement || defaultNS != null) &&
"".equals(prefix) || nsSupport.getURI(prefix) != null)) {
prefix = null;
}
if (prefix == null) {
prefix = (String) prefixTable.get(uri);
if (prefix != null &&
((!isElement || defaultNS != null) &&
"".equals(prefix) || nsSupport.getURI(prefix) != null)) {
prefix = null;
}
}
if (prefix == null && qName != null && !"".equals(qName)) {
int i = qName.indexOf(':');
if (i == -1) {
if (isElement && defaultNS == null) {
prefix = "";
}
} else {
prefix = qName.substring(0, i);
}
}
for (;
prefix == null || nsSupport.getURI(prefix) != null;
prefix = "__NS" + ++prefixCounter)
;
nsSupport.declarePrefix(prefix, uri);
doneDeclTable.put(uri, prefix);
return prefix;
}
/**
* Write a raw character.
*
* @param c The character to write.
* @exception org.xml.sax.SAXException If there is an error writing
* the character, this method will throw an IOException
* wrapped in a SAXException.
*/
private void write (char c)
throws SAXException
{
try {
output.write(c);
} catch (IOException e) {
throw new SAXException(e);
}
}
/**
* Write a raw string.
*
* @param s
* @exception org.xml.sax.SAXException If there is an error writing
* the string, this method will throw an IOException
* wrapped in a SAXException
*/
private void write (String s)
throws SAXException
{
try {
output.write(s);
} catch (IOException e) {
throw new SAXException(e);
}
}
/**
* Write out an attribute list, escaping values.
*
* The names will have prefixes added to them.
*
* @param atts The attribute list to write.
* @exception org.xml.SAXException If there is an error writing
* the attribute list, this method will throw an
* IOException wrapped in a SAXException.
*/
private void writeAttributes (Attributes atts)
throws SAXException
{
int len = atts.getLength();
for (int i = 0; i < len; i++) {
char ch[] = atts.getValue(i).toCharArray();
write(' ');
writeName(atts.getURI(i), atts.getLocalName(i),
atts.getQName(i), false);
if (htmlMode &&
booleanAttribute(atts.getLocalName(i), atts.getQName(i), atts.getValue(i))) break;
write("=\"");
writeEsc(ch, 0, ch.length, true);
write('"');
}
}
private String[] booleans = {"checked", "compact", "declare", "defer",
"disabled", "ismap", "multiple",
"nohref", "noresize", "noshade",
"nowrap", "readonly", "selected"};
// Return true if the attribute is an HTML boolean from the above list.
private boolean booleanAttribute (String localName, String qName, String value)
{
String name = localName;
if (name == null) {
int i = qName.indexOf(':');
if (i != -1) name = qName.substring(i + 1, qName.length());
}
if (!name.equals(value)) return false;
for (int j = 0; j < booleans.length; j++) {
if (name.equals(booleans[j])) return true;
}
return false;
}
/**
* Write an array of data characters with escaping.
*
* @param ch The array of characters.
* @param start The starting position.
* @param length The number of characters to use.
* @param isAttVal true if this is an attribute value literal.
* @exception org.xml.SAXException If there is an error writing
* the characters, this method will throw an
* IOException wrapped in a SAXException.
*/
private void writeEsc (char ch[], int start,
int length, boolean isAttVal)
throws SAXException
{
for (int i = start; i < start + length; i++) {
switch (ch[i]) {
case '&':
write("&");
break;
case '<':
write("<");
break;
case '>':
write(">");
break;
case '\"':
if (isAttVal) {
write(""");
} else {
write('\"');
}
break;
default:
if (!unicodeMode && ch[i] > '\u007f') {
write("&#");
write(Integer.toString(ch[i]));
write(';');
} else {
write(ch[i]);
}
}
}
}
/**
* Write out the list of Namespace declarations.
*
* @exception org.xml.sax.SAXException This method will throw
* an IOException wrapped in a SAXException if
* there is an error writing the Namespace
* declarations.
*/
private void writeNSDecls ()
throws SAXException
{
Enumeration prefixes = nsSupport.getDeclaredPrefixes();
while (prefixes.hasMoreElements()) {
String prefix = (String) prefixes.nextElement();
String uri = nsSupport.getURI(prefix);
if (uri == null) {
uri = "";
}
char ch[] = uri.toCharArray();
write(' ');
if ("".equals(prefix)) {
write("xmlns=\"");
} else {
write("xmlns:");
write(prefix);
write("=\"");
}
writeEsc(ch, 0, ch.length, true);
write('\"');
}
}
/**
* Write an element or attribute name.
*
* @param uri The Namespace URI.
* @param localName The local name.
* @param qName The prefixed name, if available, or the empty string.
* @param isElement true if this is an element name, false if it
* is an attribute name.
* @exception org.xml.sax.SAXException This method will throw an
* IOException wrapped in a SAXException if there is
* an error writing the name.
*/
private void writeName (String uri, String localName,
String qName, boolean isElement)
throws SAXException
{
String prefix = doPrefix(uri, qName, isElement);
if (prefix != null && !"".equals(prefix)) {
write(prefix);
write(':');
}
if (localName != null && !"".equals(localName)) {
write(localName);
} else {
int i = qName.indexOf(':');
write(qName.substring(i + 1, qName.length()));
}
}
////////////////////////////////////////////////////////////////////
// Default LexicalHandler implementation
////////////////////////////////////////////////////////////////////
public void comment(char[] ch, int start, int length) throws SAXException
{
write("<!--");
for (int i = start; i < start + length; i++) {
write(ch[i]);
if (ch[i] == '-' && i + 1 <= start + length && ch[i+1] == '-')
write(' ');
}
write("-->");
}
public void endCDATA() throws SAXException { }
public void endDTD() throws SAXException { }
public void endEntity(String name) throws SAXException { }
public void startCDATA() throws SAXException { }
public void startDTD(String name, String publicid, String systemid) throws SAXException {
if (name == null) return; // can't cope
if (hasOutputDTD) return; // only one DTD
hasOutputDTD = true;
write("<!DOCTYPE ");
write(name);
if (systemid == null) systemid = "";
if (overrideSystem != null) systemid = overrideSystem;
char sysquote = (systemid.indexOf('"') != -1) ? '\'': '"';
if (overridePublic != null) publicid = overridePublic;
if (!(publicid == null || "".equals(publicid))) {
char pubquote = (publicid.indexOf('"') != -1) ? '\'': '"';
write(" PUBLIC ");
write(pubquote);
write(publicid);
write(pubquote);
write(' ');
}
else {
write(" SYSTEM ");
}
write(sysquote);
write(systemid);
write(sysquote);
write(">\n");
}
public void startEntity(String name) throws SAXException { }
////////////////////////////////////////////////////////////////////
// Output properties
////////////////////////////////////////////////////////////////////
public String getOutputProperty(String key) {
return outputProperties.getProperty(key);
}
public void setOutputProperty(String key, String value) {
outputProperties.setProperty(key, value);
// System.out.println("%%%% key = [" + key + "] value = [" + value +"]");
if (key.equals(ENCODING)) {
outputEncoding = value;
unicodeMode = value.substring(0, 3).equalsIgnoreCase("utf");
// System.out.println("%%%% unicodeMode = " + unicodeMode);
}
else if (key.equals(METHOD)) {
htmlMode = value.equals("html");
}
else if (key.equals(DOCTYPE_PUBLIC)) {
overridePublic = value;
forceDTD = true;
}
else if (key.equals(DOCTYPE_SYSTEM)) {
overrideSystem = value;
forceDTD = true;
}
else if (key.equals(VERSION)) {
version = value;
}
else if (key.equals(STANDALONE)) {
standalone = value;
}
// System.out.println("%%%% htmlMode = " + htmlMode);
}
////////////////////////////////////////////////////////////////////
// Constants.
////////////////////////////////////////////////////////////////////
private final Attributes EMPTY_ATTS = new AttributesImpl();
public static final String CDATA_SECTION_ELEMENTS =
"cdata-section-elements";
public static final String DOCTYPE_PUBLIC = "doctype-public";
public static final String DOCTYPE_SYSTEM = "doctype-system";
public static final String ENCODING = "encoding";
public static final String INDENT = "indent"; // currently ignored
public static final String MEDIA_TYPE = "media-type"; // currently ignored
public static final String METHOD = "method"; // currently html or xml
public static final String OMIT_XML_DECLARATION = "omit-xml-declaration";
public static final String STANDALONE = "standalone"; // currently ignored
public static final String VERSION = "version";
////////////////////////////////////////////////////////////////////
// Internal state.
////////////////////////////////////////////////////////////////////
private Hashtable prefixTable;
private Hashtable forcedDeclTable;
private Hashtable doneDeclTable;
private int elementLevel = 0;
private Writer output;
private NamespaceSupport nsSupport;
private int prefixCounter = 0;
private Properties outputProperties;
private boolean unicodeMode = false;
private String outputEncoding = "";
private boolean htmlMode = false;
private boolean forceDTD = false;
private boolean hasOutputDTD = false;
private String overridePublic = null;
private String overrideSystem = null;
private String version = null;
private String standalone = null;
private boolean cdataElement = false;
}
// end of XMLWriter.java
Related examples in the same category