/* * Copyright (C) 2004, 2005, 2006 Joe Walnes. * Copyright (C) 2006, 2007 XStream Committers. * All rights reserved. * * The software in this package is published under the terms of the BSD * style license a copy of which has been included with this distribution in * the LICENSE.txt file. * * Created on 14. August 2004 by Joe Walnes */ package com.thoughtworks.xstream.io.xml; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.io.StreamException; import org.xml.sax.ContentHandler; import org.xml.sax.DTDHandler; import org.xml.sax.EntityResolver; import org.xml.sax.ErrorHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXNotRecognizedException; import org.xml.sax.SAXNotSupportedException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.AttributesImpl; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; /** * A SAX {@link org.xml.sax.XMLReader parser} that acts as an XStream * {@link com.thoughtworks.xstream.io.HierarchicalStreamWriter} to enable direct generation of a * SAX event flow from the XStream serialization of a list of list of Java objects. <p/> As a * custom SAX parser, this class ignores the arguments of the two standard parse methods ({@link #parse(java.lang.String)} * and {@link #parse(org.xml.sax.InputSource)}) but relies on a proprietary SAX property * {@link #SOURCE_OBJECT_LIST_PROPERTY} to define the list of objects to serialize. * </p> * <p/> Configuration of this SAX parser is achieved through the standard * {@link #setProperty SAX property mechanism}. While specific setter methods require direct * access to the parser instance, SAX properties support configuration settings to be propagated * through a chain of {@link org.xml.sax.XMLFilter filters} down to the underlying parser * object. * </p> * <p/> This mechanism shall be used to configure the * {@link #SOURCE_OBJECT_LIST_PROPERTY objects to be serialized} as well as the * {@link #CONFIGURED_XSTREAM_PROPERTY XStream facade}. * </p> * * @author Laurent Bihanic */ public final class SaxWriter extends AbstractXmlWriter implements XMLReader { /** * The {@link #setProperty SAX property} to configure the XStream * facade to be used for object serialization. If the property * is not set, a new XStream facade will be allocated for each * parse. */ public final static String CONFIGURED_XSTREAM_PROPERTY = "http://com.thoughtworks.xstream/sax/property/configured-xstream"; /** * The {@link #setProperty SAX property} to configure the list of * Java objects to serialize. Setting this property prior * invoking one of the parse() methods is mandatory. * * @see #parse(java.lang.String) * @see #parse(org.xml.sax.InputSource) */ public final static String SOURCE_OBJECT_LIST_PROPERTY = "http://com.thoughtworks.xstream/sax/property/source-object-list"; //========================================================================= // SAX XMLReader interface support //========================================================================= /** * The SAX EntityResolver associated to this XMLReader. */ private EntityResolver entityResolver = null; /** * The SAX DTDHandler associated to this XMLReader. */ private DTDHandler dtdHandler = null; /** * The SAX ContentHandler associated to this XMLReader. */ private ContentHandler contentHandler = null; /** * The SAX ErrorHandler associated to this XMLReader. */ private ErrorHandler errorHandler = null; /** * The SAX features defined for this XMLReader. * <p/> * This class does not define any feature (yet) and ignores * the SAX mandatory feature. Thus, this member is present * only to support the mandatory feature setting and retrieval * logic defined by SAX.</p> */ private Map features = new HashMap(); /** * The SAX properties defined for this XMLReader. */ private final Map properties = new HashMap(); private final boolean includeEnclosingDocument; public SaxWriter(XmlFriendlyReplacer replacer) { this(true, replacer); } public SaxWriter(boolean includeEnclosingDocument, XmlFriendlyReplacer replacer) { super(replacer); this.includeEnclosingDocument = includeEnclosingDocument; } public SaxWriter(boolean includeEnclosingDocument) { this(includeEnclosingDocument, new XmlFriendlyReplacer()); } public SaxWriter() { this(true); } //------------------------------------------------------------------------- // Configuration //------------------------------------------------------------------------- /** * Sets the state of a feature. * <p/> * The feature name is any fully-qualified URI.</p> * <p/> * All XMLReaders are required to support setting * <code>http://xml.org/sax/features/namespaces</code> to * <code>true</code> and * <code>http://xml.org/sax/features/namespace-prefixes</code> to * <code>false</code>.</p> * <p/> * Some feature values may be immutable or mutable only * in specific contexts, such as before, during, or after * a parse.</p> * <p/> * <strong>Note</strong>: This implementation only supports the two * mandatory SAX features.</p> * * @param name the feature name, which is a fully-qualified URI. * @param value the requested state of the feature (true or false). * @throws SAXNotRecognizedException when the XMLReader does not * recognize the feature name. * @see #getFeature */ public void setFeature(String name, boolean value) throws SAXNotRecognizedException { if ((name.equals("http://xml.org/sax/features/namespaces")) || (name.equals("http://xml.org/sax/features/namespace-prefixes"))) { this.features.put(name, value ? Boolean.TRUE : Boolean.FALSE); // JDK 1.3 friendly } else { throw new SAXNotRecognizedException(name); } } /** * Looks up the value of a feature. * <p/> * The feature name is any fully-qualified URI. It is * possible for an XMLReader to recognize a feature name but * to be unable to return its value; this is especially true * in the case of an adapter for a SAX1 Parser, which has * no way of knowing whether the underlying parser is * performing validation or expanding external entities.</p> * <p/> * All XMLReaders are required to recognize the * <code>http://xml.org/sax/features/namespaces</code> and the * <code>http://xml.org/sax/features/namespace-prefixes</code> feature * names.</p> * <p/> * Some feature values may be available only in specific * contexts, such as before, during, or after a parse.</p> * <p/> * Implementors are free (and encouraged) to invent their own * features, using names built on their own URIs.</p> * * @param name the feature name, which is a fully-qualified URI. * @return the current state of the feature (true or false). * @throws SAXNotRecognizedException when the XMLReader does not * recognize the feature name. * @see #setFeature */ public boolean getFeature(String name) throws SAXNotRecognizedException { if ((name.equals("http://xml.org/sax/features/namespaces")) || (name.equals("http://xml.org/sax/features/namespace-prefixes"))) { Boolean value = (Boolean) (this.features.get(name)); if (value == null) { value = Boolean.FALSE; } return value.booleanValue(); } else { throw new SAXNotRecognizedException(name); } } /** * Sets the value of a property. * <p/> * The property name is any fully-qualified URI. It is * possible for an XMLReader to recognize a property name but * to be unable to set its value.</p> * <p/> * XMLReaders are not required to recognize setting any * specific property names, though a core set is provided with * SAX2.</p> * <p/> * Some property values may be immutable or mutable only * in specific contexts, such as before, during, or after * a parse.</p> * <p/> * This method is also the standard mechanism for setting * extended handlers.</p> * <p/> * <strong>Note</strong>: This implementation only supports two * (proprietary) properties: {@link #CONFIGURED_XSTREAM_PROPERTY} * and {@link #SOURCE_OBJECT_LIST_PROPERTY}.</p> * * @param name the property name, which is a fully-qualified URI. * @param value the requested value for the property. * @throws SAXNotRecognizedException when the XMLReader does not * recognize the property name. * @throws SAXNotSupportedException when the XMLReader recognizes * the property name but cannot set * the requested value. * @see #getProperty */ public void setProperty(String name, Object value) throws SAXNotRecognizedException, SAXNotSupportedException { if (name.equals(CONFIGURED_XSTREAM_PROPERTY)) { if (!(value instanceof XStream)) { throw new SAXNotSupportedException("Value for property \"" + CONFIGURED_XSTREAM_PROPERTY + "\" must be a non-null XStream object"); } } else if (name.equals(SOURCE_OBJECT_LIST_PROPERTY)) { if (value instanceof List) { List list = (List) value; if (list.isEmpty()) { throw new SAXNotSupportedException("Value for property \"" + SOURCE_OBJECT_LIST_PROPERTY + "\" shall not be an empty list"); } else { // Perform a copy of the list to prevent the application to // modify its content while the parse is being performed. value = Collections.unmodifiableList(new ArrayList(list)); } } else { throw new SAXNotSupportedException("Value for property \"" + SOURCE_OBJECT_LIST_PROPERTY + "\" must be a non-null List object"); } } else { throw new SAXNotRecognizedException(name); } this.properties.put(name, value); } /** * Looks up the value of a property. * <p/> * The property name is any fully-qualified URI. It is * possible for an XMLReader to recognize a property name but * to be unable to return its state.</p> * <p/> * XMLReaders are not required to recognize any specific * property names, though an initial core set is documented for * SAX2.</p> * <p/> * Some property values may be available only in specific * contexts, such as before, during, or after a parse.</p> * <p/> * Implementors are free (and encouraged) to invent their own properties, * using names built on their own URIs.</p> * * @param name the property name, which is a fully-qualified URI. * @return the current value of the property. * @throws SAXNotRecognizedException when the XMLReader does not * recognize the property name. * @see #getProperty */ public Object getProperty(String name) throws SAXNotRecognizedException { if ((name.equals(CONFIGURED_XSTREAM_PROPERTY)) || (name.equals(SOURCE_OBJECT_LIST_PROPERTY))) { return this.properties.get(name); } else { throw new SAXNotRecognizedException(name); } } //--------------------------------------------------------------------- // Event handlers //--------------------------------------------------------------------- /** * Allows an application to register an entity resolver. * <p/> * If the application does not register an entity resolver, * the XMLReader will perform its own default resolution.</p> * <p/> * Applications may register a new or different resolver in the * middle of a parse, and the SAX parser must begin using the new * resolver immediately.</p> * * @param resolver the entity resolver. * @throws NullPointerException if the resolver argument is * <code>null</code>. * @see #getEntityResolver */ public void setEntityResolver(EntityResolver resolver) { if (resolver == null) { throw new NullPointerException("resolver"); } this.entityResolver = resolver; return; } /** * Returns the current entity resolver. * * @return the current entity resolver, or <code>null</code> if none * has been registered. * @see #setEntityResolver */ public EntityResolver getEntityResolver() { return this.entityResolver; } /** * Allows an application to register a DTD event handler. * <p/> * If the application does not register a DTD handler, all DTD * events reported by the SAX parser will be silently ignored.</p> * <p/> * Applications may register a new or different handler in the * middle of a parse, and the SAX parser must begin using the new * handler immediately.</p> * * @param handler the DTD handler. * @throws NullPointerException if the handler argument is * <code>null</code>. * @see #getDTDHandler */ public void setDTDHandler(DTDHandler handler) { if (handler == null) { throw new NullPointerException("handler"); } this.dtdHandler = handler; return; } /** * Returns the current DTD handler. * * @return the current DTD handler, or <code>null</code> if none * has been registered. * @see #setDTDHandler */ public DTDHandler getDTDHandler() { return this.dtdHandler; } /** * Allows an application to register a content event handler. * <p/> * If the application does not register a content handler, all * content events reported by the SAX parser will be silently * ignored.</p> * <p/> * Applications may register a new or different handler in the * middle of a parse, and the SAX parser must begin using the new * handler immediately.</p> * * @param handler the content handler. * @throws NullPointerException if the handler argument is * <code>null</code>. * @see #getContentHandler */ public void setContentHandler(ContentHandler handler) { if (handler == null) { throw new NullPointerException("handler"); } this.contentHandler = handler; return; } /** * Returns the current content handler. * * @return the current content handler, or <code>null</code> if none * has been registered. * @see #setContentHandler */ public ContentHandler getContentHandler() { return this.contentHandler; } /** * Allows an application to register an error event handler. * <p/> * If the application does not register an error handler, all * error events reported by the SAX parser will be silently * ignored; however, normal processing may not continue. It is * highly recommended that all SAX applications implement an * error handler to avoid unexpected bugs.</p> * <p/> * Applications may register a new or different handler in the * middle of a parse, and the SAX parser must begin using the new * handler immediately.</p> * * @param handler the error handler. * @throws NullPointerException if the handler argument is * <code>null</code>. * @see #getErrorHandler */ public void setErrorHandler(ErrorHandler handler) { if (handler == null) { throw new NullPointerException("handler"); } this.errorHandler = handler; return; } /** * Returns the current error handler. * * @return the current error handler, or <code>null</code> if none * has been registered. * @see #setErrorHandler */ public ErrorHandler getErrorHandler() { return this.errorHandler; } //--------------------------------------------------------------------- // Parsing //--------------------------------------------------------------------- /** * Parses an XML document from a system identifier (URI). * <p/> * This method is a shortcut for the common case of reading a * document from a system identifier. It is the exact * equivalent of the following:</p> * <blockquote> * <pre> * parse(new InputSource(systemId)); * </pre> * </blockquote> * <p/> * If the system identifier is a URL, it must be fully resolved * by the application before it is passed to the parser.</p> * <p/> * <strong>Note</strong>: As a custom SAX parser, this class * ignores the <code>systemId</code> argument of this method * and relies on the proprietary SAX property * {@link #SOURCE_OBJECT_LIST_PROPERTY}) to define the list of * objects to serialize.</p> * * @param systemId the system identifier (URI). * @throws SAXException Any SAX exception, possibly wrapping * another exception. * @see #parse(org.xml.sax.InputSource) */ public void parse(String systemId) throws SAXException { this.parse(); } /** * Parse an XML document. * <p/> * The application can use this method to instruct the XML * reader to begin parsing an XML document from any valid input * source (a character stream, a byte stream, or a URI).</p> * <p/> * Applications may not invoke this method while a parse is in * progress (they should create a new XMLReader instead for each * nested XML document). Once a parse is complete, an * application may reuse the same XMLReader object, possibly * with a different input source.</p> * <p/> * During the parse, the XMLReader will provide information * about the XML document through the registered event * handlers.</p> * <p/> * This method is synchronous: it will not return until parsing * has ended. If a client application wants to terminate * parsing early, it should throw an exception.</p> * <p/> * <strong>Note</strong>: As a custom SAX parser, this class * ignores the <code>source</code> argument of this method * and relies on the proprietary SAX property * {@link #SOURCE_OBJECT_LIST_PROPERTY}) to define the list of * objects to serialize.</p> * * @param input The input source for the top-level of the * XML document. * @throws SAXException Any SAX exception, possibly wrapping * another exception. * @see org.xml.sax.InputSource * @see #parse(java.lang.String) * @see #setEntityResolver * @see #setDTDHandler * @see #setContentHandler * @see #setErrorHandler */ public void parse(InputSource input) throws SAXException { this.parse(); } /** * Serializes the Java objects of the configured list into a flow * of SAX events. * * @throws SAXException if the configured object list is invalid * or object serialization failed. */ private void parse() throws SAXException { XStream xstream = (XStream) (this.properties.get(CONFIGURED_XSTREAM_PROPERTY)); if (xstream == null) { xstream = new XStream(); } List source = (List) (this.properties.get(SOURCE_OBJECT_LIST_PROPERTY)); if ((source == null) || (source.isEmpty())) { throw new SAXException("Missing or empty source object list. Setting property \"" + SOURCE_OBJECT_LIST_PROPERTY + "\" is mandatory"); } try { this.startDocument(true); for (Iterator i = source.iterator(); i.hasNext();) { xstream.marshal(i.next(), this); } this.endDocument(true); } catch (StreamException e) { if (e.getCause() instanceof SAXException) { throw (SAXException) (e.getCause()); } else { throw new SAXException(e); } } } //========================================================================= // XStream HierarchicalStreamWriter interface support //========================================================================= private int depth = 0; private List elementStack = new LinkedList(); private char[] buffer = new char[128]; private boolean startTagInProgress = false; private final AttributesImpl attributeList = new AttributesImpl(); public void startNode(String name) { try { if (this.depth != 0) { this.flushStartTag(); } else if (includeEnclosingDocument) { this.startDocument(false); } this.elementStack.add(0, escapeXmlName(name)); this.startTagInProgress = true; this.depth++; } catch (SAXException e) { throw new StreamException(e); } } public void addAttribute(String name, String value) { if (this.startTagInProgress) { String escapedName = escapeXmlName(name); this.attributeList.addAttribute("", escapedName, escapedName, "CDATA", value); } else { throw new StreamException(new IllegalStateException("No startElement being processed")); } } public void setValue(String text) { try { this.flushStartTag(); int lg = text.length(); if (lg > buffer.length) { buffer = new char[lg]; } text.getChars(0, lg, buffer, 0); this.contentHandler.characters(buffer, 0, lg); } catch (SAXException e) { throw new StreamException(e); } } public void endNode() { try { this.flushStartTag(); String tagName = (String) (this.elementStack.remove(0)); this.contentHandler.endElement("", tagName, tagName); this.depth--; if (this.depth == 0 && includeEnclosingDocument) { this.endDocument(false); } } catch (SAXException e) { throw new StreamException(e); } } /** * Fires the SAX startDocument event towards the configured * ContentHandler. * * @param multiObjectMode whether serialization of several * object will be merge into a single * SAX document. * @throws SAXException if thrown by the ContentHandler. */ private void startDocument(boolean multiObjectMode) throws SAXException { if (this.depth == 0) { // Notify contentHandler of document start. this.contentHandler.startDocument(); if (multiObjectMode) { // Prevent marshalling of each object to fire its own // start/endDocument events. this.depth++; } } } /** * Fires the SAX endDocument event towards the configured * ContentHandler. * * @param multiObjectMode whether serialization of several * object will be merge into a single * SAX document. * @throws SAXException if thrown by the ContentHandler. */ private void endDocument(boolean multiObjectMode) throws SAXException { if ((this.depth == 0) || ((this.depth == 1) && (multiObjectMode))) { this.contentHandler.endDocument(); this.depth = 0; } } /** * Fires any pending SAX startElement event towards the * configured ContentHandler. * * @throws SAXException if thrown by the ContentHandler. */ private void flushStartTag() throws SAXException { if (this.startTagInProgress) { String tagName = (String) (this.elementStack.get(0)); this.contentHandler.startElement("", tagName, tagName, this.attributeList); this.attributeList.clear(); this.startTagInProgress = false; } } public void flush() { // don't need to do anything } public void close() { // don't need to do anything } }