You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jackrabbit.apache.org by ju...@apache.org on 2008/01/28 21:34:36 UTC

svn commit: r616030 - in /jackrabbit/trunk/jackrabbit-jcr-commons/src: main/java/org/apache/jackrabbit/commons/SerializingContentHandler.java test/java/org/apache/jackrabbit/commons/SerializingContentHandlerTest.java

Author: jukka
Date: Mon Jan 28 12:34:34 2008
New Revision: 616030

URL: http://svn.apache.org/viewvc?rev=616030&view=rev
Log:
JCR-1350: Add a serializing content handler

Added:
    jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/SerializingContentHandler.java
      - copied, changed from r616012, cocoon/trunk/core/cocoon-pipeline/cocoon-pipeline-impl/src/main/java/org/apache/cocoon/serialization/AbstractTextSerializer.java
    jackrabbit/trunk/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/SerializingContentHandlerTest.java

Copied: jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/SerializingContentHandler.java (from r616012, cocoon/trunk/core/cocoon-pipeline/cocoon-pipeline-impl/src/main/java/org/apache/cocoon/serialization/AbstractTextSerializer.java)
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/SerializingContentHandler.java?p2=jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/SerializingContentHandler.java&p1=cocoon/trunk/core/cocoon-pipeline/cocoon-pipeline-impl/src/main/java/org/apache/cocoon/serialization/AbstractTextSerializer.java&r1=616012&r2=616030&rev=616030&view=diff
==============================================================================
--- cocoon/trunk/core/cocoon-pipeline/cocoon-pipeline-impl/src/main/java/org/apache/cocoon/serialization/AbstractTextSerializer.java (original)
+++ jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/SerializingContentHandler.java Mon Jan 28 12:34:34 2008
@@ -14,447 +14,80 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.cocoon.serialization;
+package org.apache.jackrabbit.commons;
 
 import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Properties;
-import javax.xml.transform.OutputKeys;
-import javax.xml.transform.TransformerException;
-import javax.xml.transform.TransformerFactory;
-import javax.xml.transform.TransformerFactoryConfigurationError;
+
+import javax.xml.transform.Result;
+import javax.xml.transform.TransformerConfigurationException;
 import javax.xml.transform.sax.SAXTransformerFactory;
 import javax.xml.transform.sax.TransformerHandler;
 import javax.xml.transform.stream.StreamResult;
 
-import org.apache.avalon.framework.configuration.Configurable;
-import org.apache.avalon.framework.configuration.Configuration;
-import org.apache.avalon.framework.configuration.ConfigurationException;
-import org.apache.avalon.framework.service.ServiceException;
-import org.apache.avalon.framework.service.ServiceManager;
-import org.apache.avalon.framework.service.Serviceable;
-import org.apache.commons.lang.BooleanUtils;
-import org.apache.excalibur.source.SourceValidity;
-import org.apache.excalibur.source.impl.validity.NOPValidity;
-
-import org.apache.cocoon.Constants;
-import org.apache.cocoon.caching.CacheableProcessingComponent;
-import org.apache.cocoon.configuration.Settings;
-import org.apache.cocoon.util.ClassUtils;
-import org.apache.cocoon.util.TraxErrorHandler;
-import org.apache.cocoon.xml.AbstractXMLPipe;
-import org.apache.cocoon.xml.XMLConsumer;
-import org.apache.cocoon.xml.XMLUtils;
-
 import org.xml.sax.Attributes;
 import org.xml.sax.ContentHandler;
 import org.xml.sax.SAXException;
-import org.xml.sax.ext.LexicalHandler;
 import org.xml.sax.helpers.AttributesImpl;
 
 /**
- * @version $Id$
+ * A {@link ContentHandler} that serializes SAX events to a given
+ * {@link Result} instance. The JAXP {@link SAXTransformerFactory}
+ * facility is used for the serialization. This class handles also cases
+ * problems like the Xalan serializer in JDK 1.4 not outputting correct
+ * xmlns attributes.
  */
-public abstract class AbstractTextSerializer extends AbstractSerializer
-                                             implements Configurable, Serviceable, CacheableProcessingComponent {
-    
-    /**
-     * Cache for avoiding unnecessary checks of namespaces abilities.
-     * It associates a Boolean to the transformer class name.
-     */
-    private static final Map needsNamespaceCache = new HashMap();
-
-    /**
-     * The trax <code>TransformerFactory</code> used by this serializer.
-     */
-    private SAXTransformerFactory tfactory;
-
-    /**
-     * The <code>Properties</code> used by this serializer.
-     */
-    protected Properties format = new Properties();
-
-    /**
-     * The pipe that adds namespaces as xmlns attributes.
-     */
-    private NamespaceAsAttributes namespacePipe;
-
-    /**
-     * The caching key
-     */
-    private String cachingKey;
-
-    private String transformerFactoryClass;
-    
-    private String defaultEncoding = null;
-
-    /**
-     * Set the properties used for transformer handler used by the serializer
-     * see {@link OutputKeys} for possible settings.
-     * 
-     * @param format
-     */
-    public void setFormat(Properties format) {
-        this.format = format;
-    }
-
-    /**
-     * Set the default encoding. This will be overided if the encoding is set
-     * in the format properties. This is mainly useful together with Spring
-     * bean inheritance.
-     * 
-     * @param defaultEncoding
-     */
-    public void setDefaultEncoding(String defaultEncoding) {
-        this.defaultEncoding = defaultEncoding;
-    }
-
-    /**
-     * Initialize logger, caching key, transformer handler and namespace pipe
-     *
-     * @throws Exception 
-     */
-    public void init() throws Exception {
-        if (!this.format.containsKey(OutputKeys.ENCODING) && this.defaultEncoding != null) {
-            this.format.put(OutputKeys.ENCODING, this.defaultEncoding);
-        }
-        this.cachingKey = createCachingKey(format);
-        initTransformerFactory();
-        initNamespacePipe();
-    }
-
-    /**
-     * Optionally set the transformer factory used for creating the transformer
-     * handler that is used for serialization. Otherwise the standard transformer
-     * factory is used ({@link TransformerFactory}).
-     * 
-     * @param transformerFactoryClass the name of the class
-     */
-    public void setTransformerFactory(String transformerFactoryClass) {
-        this.transformerFactoryClass = transformerFactoryClass;
-    }
-
-    /**
-     * Interpose namespace pipe if needed.
-     */
-    public void setConsumer(XMLConsumer consumer) {
-        if (this.namespacePipe == null) {
-            super.setConsumer(consumer);
-        } else {
-            this.namespacePipe.setConsumer(consumer);
-            super.setConsumer(this.namespacePipe);
-        }
-    }
-
-    /**
-     * Interpose namespace pipe if needed.
-     */
-    public void setContentHandler(ContentHandler handler) {
-        if (this.namespacePipe == null) {
-            super.setContentHandler(handler);
-        } else {
-            this.namespacePipe.setContentHandler(handler);
-            super.setContentHandler(this.namespacePipe);
-        }
-    }
-
-    /**
-     * Interpose namespace pipe if needed.
-     */
-    public void setLexicalHandler(LexicalHandler handler) {
-        if (this.namespacePipe == null) {
-            super.setLexicalHandler(handler);
-        } else {
-            this.namespacePipe.setLexicalHandler(handler);
-            super.setLexicalHandler(this.namespacePipe);
-        }
-    }
-
-    /**
-     * Helper for TransformerFactory.
-     */
-    protected SAXTransformerFactory getTransformerFactory() {
-        return tfactory;
-    }
-
-    /**
-     * Helper for TransformerHandler.
-     */
-    protected TransformerHandler getTransformerHandler() throws TransformerException {
-        return this.getTransformerFactory().newTransformerHandler();
-    }
-
-//    /**
-//     * Set the {@link OutputStream} where the requested resource should
-//     * be serialized.
-//     */
-//    public void setOutputStream(OutputStream out) throws IOException {
-//        /*
-//         * Add a level of buffering to the output stream. Xalan serializes
-//         * every character individually. In conjunction with chunked
-//         * transfer encoding this would otherwise lead to a whopping 6-fold
-//         * increase of data on the wire.
-//         */
-//        //  if (outputBufferSize > 0) {
-//        //      super.setOutputStream(
-//        //        new BufferedOutputStream(out, outputBufferSize));
-//        //  } else {
-//        super.setOutputStream(out);
-//        //  }
-//    }
-
-    /**
-     * @see org.apache.avalon.framework.service.Serviceable#service(org.apache.avalon.framework.service.ServiceManager)
-     * 
-     * @deprecated use property injection instead
-     */
-    public void service(ServiceManager manager) throws ServiceException {
-        final Settings settings = (Settings)manager.lookup(Settings.ROLE);
-        String defaultEncoding  = settings.getFormEncoding();
-        if (defaultEncoding != null) {
-            this.format.setProperty(OutputKeys.ENCODING, defaultEncoding);
-        }
-        manager.release(settings);
-    }
-
-    /**
-     * Set the configurations for this serializer.
-     * 
-     * @deprecated use property injection instead
-     */
-    public void configure(Configuration conf) throws ConfigurationException {
-        // configure buffer size
-        //   Configuration bsc = conf.getChild("buffer-size", false);
-        //   if(null != bsc)
-        //    outputBufferSize = bsc.getValueAsInteger(DEFAULT_BUFFER_SIZE);
-
-        // configure xalan
-        String cdataSectionElements = conf.getChild("cdata-section-elements").getValue(null);
-        String dtPublic = conf.getChild("doctype-public").getValue(null);
-        String dtSystem = conf.getChild("doctype-system").getValue(null);
-        String encoding = conf.getChild("encoding").getValue(null);
-        String indent = conf.getChild("indent").getValue(null);
-        String mediaType = conf.getChild("media-type").getValue(null);
-        String method = conf.getChild("method").getValue(null);
-        String omitXMLDeclaration = conf.getChild("omit-xml-declaration").getValue(null);
-        String standAlone = conf.getChild("standalone").getValue(null);
-        String version = conf.getChild("version").getValue(null);
-
-        if (cdataSectionElements != null) {
-            format.put(OutputKeys.CDATA_SECTION_ELEMENTS, cdataSectionElements);
-        }
-        if (dtPublic != null) {
-            format.put(OutputKeys.DOCTYPE_PUBLIC, dtPublic);
-        }
-        if (dtSystem != null) {
-            format.put(OutputKeys.DOCTYPE_SYSTEM, dtSystem);
-        }
-        if (encoding != null) {
-            format.put(OutputKeys.ENCODING, encoding);
-        }
-        if (indent != null) {
-            format.put(OutputKeys.INDENT, indent);
-        }
-        if (mediaType != null) {
-            format.put(OutputKeys.MEDIA_TYPE, mediaType);
-        }
-        if (method != null) {
-            format.put(OutputKeys.METHOD, method);
-        }
-        if (omitXMLDeclaration != null) {
-            format.put(OutputKeys.OMIT_XML_DECLARATION, omitXMLDeclaration);
-        }
-        if (standAlone != null) {
-            format.put(OutputKeys.STANDALONE, standAlone);
-        }
-        if (version != null) {
-            format.put(OutputKeys.VERSION, version);
-        }
+public class SerializingContentHandler extends DefaultContentHandler {
 
-        this.cachingKey = createCachingKey(format);
-
-        this.transformerFactoryClass = conf.getChild("transformer-factory").getValue(null);
-        this.initTransformerFactory();
-
-        this.initNamespacePipe();
-
-    }
-
-    /**
-     * @see org.apache.avalon.excalibur.pool.Recyclable#recycle()
-     */
-    public void recycle() {
-        super.recycle();
-        if (this.namespacePipe != null) {
-            this.namespacePipe.recycle();
-        }
-    }
+    public static ContentHandler getSerializer(Result result)
+            throws SAXException, TransformerConfigurationException {
+        SAXTransformerFactory factory = (SAXTransformerFactory)
+            SAXTransformerFactory.newInstance();
+
+        TransformerHandler handler = factory.newTransformerHandler();
+        handler.setResult(result);
+
+        TransformerHandler probe = factory.newTransformerHandler();
+        StringWriter writer = new StringWriter();
+        probe.setResult(new StreamResult(writer));
+        probe.startDocument();
+        probe.startPrefixMapping("p", "uri");
+        probe.startElement("uri", "e", "p:e", new AttributesImpl());
+        probe.endElement("uri", "e", "p:e");
+        probe.endPrefixMapping("p");
+        probe.endDocument();
 
-    private void initTransformerFactory() throws ConfigurationException, TransformerFactoryConfigurationError {
-        if (transformerFactoryClass != null) {
-            try {
-                this.tfactory = (SAXTransformerFactory) ClassUtils.newInstance(transformerFactoryClass);
-                if (getLogger().isDebugEnabled()) {
-                    getLogger().debug("Using transformer factory " + transformerFactoryClass);
-                }
-            } catch (Exception e) {
-                throw new ConfigurationException("Cannot load transformer factory " + transformerFactoryClass, e);
-            }
+        if (writer.toString().indexOf("xmlns") == -1) {
+            return new NamespaceAsAttributes(handler);
         } else {
-            // Standard TrAX behaviour
-            this.tfactory = (SAXTransformerFactory) TransformerFactory.newInstance();
+            return handler;
         }
-        tfactory.setErrorListener(new TraxErrorHandler(getLogger()));
     }
 
-    private void initNamespacePipe() {
-        // Check if we need namespace as attributes.
-        try {
-            if (needsNamespacesAsAttributes()) {
-                // Setup a correction pipe
-                this.namespacePipe = new NamespaceAsAttributes();
-            }
-        } catch (Exception e) {
-            getLogger().warn("Cannot know if transformer needs namespaces attributes - assuming NO.", e);
-        }
+    public SerializingContentHandler(Result result)
+            throws TransformerConfigurationException, SAXException {
+        super(getSerializer(result));
     }
 
-    /**
-     * Create the caching key from the formating properties used by the
-     * transformer handler that is used for serialization.
-     * @param format 
-     *
-     */
-    protected static String createCachingKey(Properties format) {
-        final StringBuffer buffer = new StringBuffer();
-        String value = null;
-        
-        // Use lookup of the property values instead of just iterating through the
-        // enumeration of them to give the caching key a  deterministic order of its parts
-
-        if ((value = format.getProperty(OutputKeys.CDATA_SECTION_ELEMENTS)) != null) {
-            buffer.append(";cdata-section-elements=").append(value);
-        }
-        if ((value = format.getProperty(OutputKeys.DOCTYPE_PUBLIC)) != null) {
-            buffer.append(";doctype-public=").append(value);
-        }
-        if ((value = format.getProperty(OutputKeys.DOCTYPE_SYSTEM)) != null) {
-            buffer.append(";doctype-system=").append(value);
-        }
-        if ((value = format.getProperty(OutputKeys.ENCODING)) != null) {
-            buffer.append(";encoding=").append(value);
-        }
-        if ((value = format.getProperty(OutputKeys.INDENT)) != null) {
-            buffer.append(";indent=").append(value);
-        }
-        if ((value = format.getProperty(OutputKeys.MEDIA_TYPE)) != null) {
-            buffer.append(";media-type=").append(value);
-        }
-        if ((value = format.getProperty(OutputKeys.METHOD)) != null) {
-            buffer.append(";method=").append(value);
-        }
-        if ((value = format.getProperty(OutputKeys.OMIT_XML_DECLARATION)) != null) {
-            buffer.append(";omit-xml-declaration=").append(value);
-        }
-        if ((value = format.getProperty(OutputKeys.STANDALONE)) != null) {
-            buffer.append(";standalone=").append(value);
-        }
-        if ((value = format.getProperty(OutputKeys.VERSION)) != null) {
-            buffer.append(";version=").append(value);
-        }
-
-        if ( buffer.length() > 0 )
-            return buffer.toString();
-        else
-            return "1";
-    }
-
-    /**
-     * Generate the unique key.
-     * This key must be unique inside the space of this component.
-     * This method must be invoked before the generateValidity() method.
-     *
-     * @return The generated key or <code>0</code> if the component
-     *              is currently not cacheable.
-     */
-    public java.io.Serializable getKey() {
-        return this.cachingKey;
-    }
-
-    /**
-     * Generate the validity object.
-     * Before this method can be invoked the generateKey() method
-     * must be invoked.
-     *
-     * @return The generated validity object or <code>null</code> if the
-     *         component is currently not cacheable.
-     */
-    public SourceValidity getValidity() {
-        return NOPValidity.SHARED_INSTANCE;
-    }
-
-    /**
-     * Checks if the used Trax implementation correctly handles namespaces set using
-     * <code>startPrefixMapping()</code>, but wants them also as 'xmlns:' attributes.
-     * <p>
-     * The check consists in sending SAX events representing a minimal namespaced document
-     * with namespaces defined only with calls to <code>startPrefixMapping</code> (no
-     * xmlns:xxx attributes) and check if they are present in the resulting text.
-     */
-    protected boolean needsNamespacesAsAttributes() throws Exception {
-
-        SAXTransformerFactory factory = getTransformerFactory();
-
-        Boolean cacheValue = (Boolean) needsNamespaceCache.get(factory.getClass().getName());
-        if (cacheValue != null) {
-            return cacheValue.booleanValue();
-        } else {
-            // Serialize a minimal document to check how namespaces are handled.
-            StringWriter writer = new StringWriter();
-
-            String uri = "namespaceuri";
-            String prefix = "nsp";
-            String check = "xmlns:" + prefix + "='" + uri + "'";
-
-            TransformerHandler handler = this.getTransformerHandler();
-
-            handler.getTransformer().setOutputProperties(format);
-            handler.setResult(new StreamResult(writer));
-
-            // Output a single element
-            handler.startDocument();
-            handler.startPrefixMapping(prefix, uri);
-            handler.startElement(uri, "element", "element", XMLUtils.EMPTY_ATTRIBUTES);
-            handler.endElement(uri, "element", "element");
-            handler.endPrefixMapping(prefix);
-            handler.endDocument();
-
-            String text = writer.toString();
-
-            // Check if the namespace is there (replace " by ' to be sure of what we search in)
-            boolean needsIt = (text.replace('"', '\'').indexOf(check) == -1);
-
-            String msg = needsIt ? " needs namespace attributes (will be slower)." : " handles correctly namespaces.";
-
-            getLogger().debug("Trax handler " + handler.getClass().getName() + msg);
-
-            needsNamespaceCache.put(factory.getClass().getName(), BooleanUtils.toBooleanObject(needsIt));
-
-            return needsIt;
-        }
-    }
-
-    //--------------------------------------------------------------------------------------------
 
     /**
      * A pipe that ensures that all namespace prefixes are also present as
      * 'xmlns:' attributes. This used to circumvent Xalan's serialization behaviour
      * which is to ignore namespaces if they're not present as 'xmlns:xxx' attributes.
-     */
-    public static class NamespaceAsAttributes extends AbstractXMLPipe {
+     * <p>
+     * NOTE: This class was originally written for Apache Cocoon and is
+     * included with minor modifications here in Apache Jackrabbit. See the
+     * org.apache.cocoon.serialization.AbstractTextSerializer class in the
+     * cocoon-pipeline-impl component for the original code.
+     */
+    private static class NamespaceAsAttributes extends DefaultContentHandler {
+
+        /** The URI for xml namespaces */
+        private static final String XML_NAMESPACE_URI =
+            "http://www.w3.org/XML/1998/namespace";
 
         /**
          * The prefixes of startPrefixMapping() declarations for the coming element.
@@ -478,6 +111,10 @@
          */
         private boolean hasMappings = false;
 
+        public NamespaceAsAttributes(ContentHandler handler) {
+            super(handler);
+        }
+
         public void startDocument() throws SAXException {
             // Cleanup
             this.uriToPrefixMap.clear();
@@ -547,8 +184,6 @@
                         if (qName.equals(attrs.getQName(attr))) {
                             // Check if mapping and attribute URI match
                             if (!uri.equals(attrs.getValue(attr))) {
-                                getLogger().error("URI in prefix mapping and attribute do not match : '"
-                                                  + uri + "' - '" + attrs.getURI(attr) + "'");
                                 throw new SAXException("URI in prefix mapping and attribute do not match");
                             }
                             found = true;
@@ -569,9 +204,11 @@
                         }
 
                         if (prefix.equals("")) {
-                            newAttrs.addAttribute(Constants.XML_NAMESPACE_URI, "xmlns", "xmlns", "CDATA", uri);
+                            newAttrs.addAttribute(
+                                    XML_NAMESPACE_URI, "xmlns", "xmlns", "CDATA", uri);
                         } else {
-                            newAttrs.addAttribute(Constants.XML_NAMESPACE_URI, prefix, qName, "CDATA", uri);
+                            newAttrs.addAttribute(
+                                    XML_NAMESPACE_URI, prefix, qName, "CDATA", uri);
                         }
                     }
                 } // end for mapping
@@ -646,5 +283,7 @@
             this.prefixList.clear();
             this.uriList.clear();
         }
+
     }
+
 }

Added: jackrabbit/trunk/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/SerializingContentHandlerTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/SerializingContentHandlerTest.java?rev=616030&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/SerializingContentHandlerTest.java (added)
+++ jackrabbit/trunk/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/SerializingContentHandlerTest.java Mon Jan 28 12:34:34 2008
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.commons;
+
+import java.io.StringWriter;
+
+import javax.xml.transform.stream.StreamResult;
+
+import org.xml.sax.ContentHandler;
+import org.xml.sax.helpers.AttributesImpl;
+
+import junit.framework.TestCase;
+
+public class SerializingContentHandlerTest extends TestCase {
+
+    public void testSerializingContentHandler() throws Exception {
+        StringWriter writer = new StringWriter();
+
+        ContentHandler handler =
+            new SerializingContentHandler(new StreamResult(writer));
+        handler.startDocument();
+        handler.startPrefixMapping("p", "uri");
+        handler.startElement("uri", "a", "p:a", new AttributesImpl());
+        AttributesImpl attributes = new AttributesImpl();
+        attributes.addAttribute("uri", "foo", "p:foo", "CDATA", "bar");
+        handler.startElement(null, "b", "b", attributes);
+        handler.characters("abc".toCharArray(), 0, 3);
+        handler.endElement(null, "b", "b");
+        handler.startElement(null, "c", "c", new AttributesImpl());
+        handler.endElement(null, "c", "c");
+        handler.characters("xyz".toCharArray(), 0, 3);
+        handler.endElement("uri", "a", "p:a");
+        handler.endPrefixMapping("p");
+        handler.endDocument();
+
+        String xml = writer.toString();
+        assertContains(xml, "<p:a");
+        assertContains(xml, "xmlns:p");
+        assertContains(xml, "=");
+        assertContains(xml, "uri");
+        assertContains(xml, ">");
+        assertContains(xml, "<b");
+        assertContains(xml, "p:foo");
+        assertContains(xml, "bar");
+        assertContains(xml, "abc");
+        assertContains(xml, "</b>");
+        assertContains(xml, "<c/>");
+        assertContains(xml, "xyz");
+        assertContains(xml, "</p:a>");
+    }
+
+    private void assertContains(String haystack, String needle) {
+        if (haystack.indexOf(needle) == -1) {
+            fail("'" + haystack + "' does not contain '" + needle+ "'");
+        }
+    }
+}