dk.statsbiblioteket.util.xml.XMLStepperTest.java Source code

Java tutorial

Introduction

Here is the source code for dk.statsbiblioteket.util.xml.XMLStepperTest.java

Source

/*
 *  Licensed 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 dk.statsbiblioteket.util.xml;

import dk.statsbiblioteket.util.Profiler;
import dk.statsbiblioteket.util.Strings;
import junit.framework.TestCase;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.xml.stream.*;
import java.io.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;

public class XMLStepperTest extends TestCase {
    private static Log log = LogFactory.getLog(XMLStepperTest.class);

    private static final String SAMPLE = "<foo><bar xmlns=\"http://www.example.com/bar_ns/\">"
            + "<nam:subsub xmlns:nam=\"http://example.com/subsub_ns\">content1<!-- Sub comment --></nam:subsub>"
            + "<!-- Comment --></bar>\n" + "<bar><subsub>content2</subsub></bar></foo>";

    private static final String DERIVED_NAMESPACE = "<foo xmlns=\"http://www.example.com/foo_ns/\"><bar>simple bar</bar></foo>";

    private static final String OUTER_SNIPPET = "<bar>bar1</bar><bar>bar2</bar>";
    private static final String OUTER_FULL = "<major><foo>" + OUTER_SNIPPET + "</foo>\n<foo>next</foo></major>";

    private XMLInputFactory xmlFactory = XMLInputFactory.newInstance();
    {
        xmlFactory.setProperty(XMLInputFactory.IS_COALESCING, true);
        // No resolving of external DTDs
        xmlFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
    }
    private XMLOutputFactory xmlOutFactory = XMLOutputFactory.newInstance();

    public void testFakeXPath() throws XMLStreamException {
        final String BIG_XML = Strings
                .flushLocal(Thread.currentThread().getContextClassLoader().getResourceAsStream("data/big.xml"));
        final String[][] tests = new String[][] {
                { "/project/property/@name", "project.name", "project.version.variant" } };
        assertXPaths(BIG_XML, tests, 2);
    }

    public void testFakeXPathStar() throws XMLStreamException {
        final String BIG_XML = Strings
                .flushLocal(Thread.currentThread().getContextClassLoader().getResourceAsStream("data/big.xml"));
        final String[][] tests = new String[][] { { "/project/property/@name", "project.name" },
                { "/project/*/@name", "project.name" }, { "/*/property/@name", "project.name" },
                { "/*/*/@name", "project.name" } };
        assertXPathShorthand(BIG_XML, tests);
    }

    public void testFakeXPathPredicated() throws XMLStreamException {
        final String BIG_XML = Strings
                .flushLocal(Thread.currentThread().getContextClassLoader().getResourceAsStream("data/big.xml"));
        final String[][] tests = new String[][] { { "/project/monkey[@bar='fun']", "ape" },
                { "/project/property[@name='lib.dir']/@value", "${basedir}/lib" },
                { "/project/property[@name]/@value", "sbutil" },
                { "/project/*[@name='config.dir']/@value", "${basedir}/config" }, };
        assertXPathShorthand(BIG_XML, tests);
    }

    public void testFakeXPathAnywhere() throws XMLStreamException {
        final String BIG_XML = Strings
                .flushLocal(Thread.currentThread().getContextClassLoader().getResourceAsStream("data/big.xml"));
        final String[][] tests = new String[][] { { "//bar", "zoo2" }, { "//bar/text()", "zoo2" },
                { "/bar]", null }, { "/project/foo/bar", "zoo2", "" }, { "/project/foo/bar/text()", "zoo2", "" }, };
        assertXPathShorthand(BIG_XML, tests);
    }

    public void testFakeXPathShorthand() throws XMLStreamException {
        final String BIG_XML = Strings
                .flushLocal(Thread.currentThread().getContextClassLoader().getResourceAsStream("data/big.xml"));
        final String[][] tests = new String[][] { { "/project/property/@name", "project.name" },
                { "/project/tstamp/format/@pattern", "MM/dd/yyyy HH:mm" } };
        assertXPathShorthand(BIG_XML, tests);
        assertXPathShorthands(BIG_XML, tests);
    }

    private void assertXPathShorthand(String xml, String[][] tests) throws XMLStreamException {
        for (String[] test : tests) {
            String result = XMLStepper.evaluateFakeXPath(xml, test[0]);
            assertEquals("The single-xpath result for '" + test[0] + " should be as expected", test[1], result);
        }
    }

    private void assertXPathShorthands(String xml, String[][] tests) throws XMLStreamException {
        List<String> xPaths = new ArrayList<String>(tests.length);
        for (String[] test : tests) {
            xPaths.add(test[0]);
        }
        List<String> results = XMLStepper.evaluateFakeXPathsSingleResults(xml, xPaths);

        for (int i = 0; i < tests.length; i++) {
            String[] test = tests[i];
            String result = results.get(i);
            assertEquals("The multi-xpaths result for '" + test[0] + " should be as expected", test[1], result);
        }
    }

    public void testFakeXPathEarlyTermination() throws XMLStreamException {
        final String BIG_XML = Strings
                .flushLocal(Thread.currentThread().getContextClassLoader().getResourceAsStream("data/big.xml"));
        final String[][] tests = new String[][] { { "/project/tstamp/format/@pattern", "MM/dd/yyyy HH:mm" } };
        assertXPaths(BIG_XML, tests, 1);
    }

    public void testFakeXPathMulti() throws XMLStreamException {
        final String BIG_XML = Strings
                .flushLocal(Thread.currentThread().getContextClassLoader().getResourceAsStream("data/big.xml"));
        final String[][] tests = new String[][] {
                { "/project/property/@name", "project.name", "project.version.variant" },
                { "/project/tstamp/format/@pattern", "MM/dd/yyyy HH:mm" }, { "/project/moo", "zoo1" },
                { "/project/foo/bar", "zoo2", "" },
                // TODO: Make multi-matches work
                //{"/project/foo/bar", "zoo2"}, // Yes, repeat of the above - does not work yet
                //{"/project/foo/bar@moo", "?"},
                //{"/project/foo/bar/@somat", "zoo4"}
                { "/project/foo/baz", "zoo3" }, { "//monkey", "ape", "gorilla" } };
        assertXPaths(BIG_XML, tests, 2);
    }

    private void assertXPaths(String xml, String[][] tests, int maxMatchesPerXP) throws XMLStreamException {
        List<String> xPaths = new ArrayList<String>(tests.length);
        for (String[] test : tests) {
            xPaths.add(test[0]);
        }
        List<List<String>> results = XMLStepper.evaluateFakeXPaths(xml, xPaths, maxMatchesPerXP);

        for (int i = 0; i < tests.length; i++) {
            String[] test = tests[i];
            List<String> result = results.get(i);
            assertEquals("The number of matches for " + test[0] + " should be as expected", test.length - 1,
                    result.size());
            for (int j = 0; j < test.length - 1; j++) {
                assertEquals("Result #" + j + " for " + test[0] + " should match", test[j + 1], result.get(j));
            }
        }
    }

    public void testSpaceRemoval() throws IOException, XMLStreamException {
        String INPUT = Strings
                .flush(Thread.currentThread().getContextClassLoader().getResourceAsStream("replacement_input.xml"));
        String EXPECTED = Strings.flush(
                Thread.currentThread().getContextClassLoader().getResourceAsStream("replacement_expected.xml"));
        String actual = XMLStepper.replaceElementText(INPUT, new XMLStepper.ContentReplaceCallback() {
            @Override
            protected String replace(List<String> tags, String current, String originalText) {
                return originalText.replace(" ", "");
            }

            @Override
            protected boolean match(XMLStreamReader xml, List<String> tags, String current) {
                return "bar".equals(current) && "a".equals(XMLStepper.getAttribute(xml, "name", ""))
                        || "baz".equals(current);
            }
        });
        assertEquals("The text content replaced XML should be as expected", EXPECTED, actual);
    }

    public void testSpaceRemovalStreaming() throws IOException, XMLStreamException {
        InputStream INPUT = Thread.currentThread().getContextClassLoader()
                .getResourceAsStream("replacement_input.xml");
        String EXPECTED = Strings.flush(
                Thread.currentThread().getContextClassLoader().getResourceAsStream("replacement_expected.xml"));
        InputStream actualS = XMLStepper.streamingReplaceElementText(INPUT,
                new XMLStepper.ContentReplaceCallback() {
                    @Override
                    protected String replace(List<String> tags, String current, String originalText) {
                        return originalText.replace(" ", "");
                    }

                    @Override
                    protected boolean match(XMLStreamReader xml, List<String> tags, String current) {
                        return "bar".equals(current) && "a".equals(XMLStepper.getAttribute(xml, "name", ""))
                                || "baz".equals(current);
                    }
                });

        String actual = Strings.flush(actualS);
        // TODO: Figure out why the streaming version keeps the header, while the direct one doesn't
        actual = actual.replace("<?xml version=\"1.0\" encoding=\"UTF-8\"?>", "").trim();
        assertEquals("The text content replaced XML should be as expected", EXPECTED, actual);
    }

    public void testIsWellformed() {
        final String[] FINE = new String[] {
                "<foo xmlns=\"http://www.example.com/foo_ns/\"><bar>simple bar</bar></foo>", "<foo/>" };
        final String[] FAULTY = new String[] {
                "<foo xmlns=\"http://www.example.com/foo_ns/\"><bar>simple bar<bar></bar></foo>",
                "<foo xmlns=\"http://www.example.com/foo_ns/\"><bar>simple bar</bar>",
                "<foo xmlns=\"http://www.example.com/foo_ns/\"><bar>simple bar</bar></doo>",
                "<foo xmlns=\"http://www.example.com/foo_ns/><bar>simple bar</bar></foo>", };
        for (String fine : FINE) {
            assertTrue("The XML should be well-formed: " + fine, XMLStepper.isWellformed(fine));
        }
        for (String faulty : FAULTY) {
            assertFalse("The XML should not be well-formed: " + faulty, XMLStepper.isWellformed(faulty));
        }
    }

    public void testMultipleInner() throws XMLStreamException {
        final String XML = "<foo><bar><zoo>z1</zoo><zoo>z2</zoo></bar></foo>";
        for (final Boolean step : new boolean[] { Boolean.FALSE, Boolean.TRUE }) {
            XMLStreamReader xml = xmlFactory.createXMLStreamReader(new StringReader(XML));
            XMLStepper.findTagStart(xml, "zoo");
            final AtomicInteger zooCount = new AtomicInteger(0);
            XMLStepper.iterateTags(xml, new XMLStepper.Callback() {
                @Override
                public boolean elementStart(XMLStreamReader xml, List<String> tags, String current)
                        throws XMLStreamException {
                    if ("zoo".equals(current)) {
                        zooCount.incrementAndGet();
                    }
                    if (step) {
                        xml.next();
                        return true;
                    }
                    return false;
                }
            });
            assertEquals(
                    "After iteration with step==" + step
                            + ", the stepper should have encountered 'zoo' the right number of times",
                    2, zooCount.get());
            assertEquals(
                    "After iteration with step==" + step
                            + ", the reader should be positioned at the correct end tag",
                    "bar", xml.getLocalName());
        }

    }

    public void testLenient() throws XMLStreamException {
        final String[][] TESTS = new String[][] {
                new String[] { "<foo><bar><zoo>Hello</zoo></bar></foo>", "zoo", "bar" },
                new String[] { "<foo><bar><zoo>Hello</zoo></bar></foo>", "zoo", "foo" },
                new String[] { "<foo><bar>Hello</bar></foo>", "bar", "foo" } };
        for (String[] test : TESTS) {
            try {
                XMLStreamReader xml = xmlFactory.createXMLStreamReader(new StringReader(test[0]));
                lenientHelper(xml, false, test[1], test[2]);
                fail("Stepping past the current element with lenient==false should raise an exception for "
                        + test[0]);
            } catch (IllegalStateException e) {
                // Expected
            }
            XMLStreamReader xml = xmlFactory.createXMLStreamReader(new StringReader(test[0]));
            lenientHelper(xml, true, test[1], test[2]);
        }
    }

    private void lenientHelper(XMLStreamReader xml, boolean lenient, final String startTag,
            final String skipToEndTag) throws XMLStreamException {
        XMLStepper.iterateTags(xml, lenient, new XMLStepper.Callback() {
            @Override
            public boolean elementStart(XMLStreamReader xml, List<String> tags, String current)
                    throws XMLStreamException {
                if (startTag.equals(current)) {
                    XMLStepper.findTagEnd(xml, skipToEndTag);
                    return true;
                }
                return false;
            }
        });
    }

    private final static String LIMIT_BARS = "<foo><bar zoo=\"true\"></bar><bar zoo=\"true\"></bar><bar zoo=\"false\"></bar><baz></baz></foo>";

    public void testLimitXMLSimple() throws XMLStreamException {
        assertLimit(LIMIT_BARS, "<foo><baz /></foo>", false, true, false, "/foo/bar", 0);
        assertLimit(LIMIT_BARS, "<foo><bar zoo=\"true\" /><baz /></foo>", false, true, false, "/foo/bar", 1);
        assertLimit(LIMIT_BARS, "<foo><bar zoo=\"true\" /><bar zoo=\"true\" /><baz /></foo>", false, true, false,
                "/foo/bar", 2);
    }

    public void testLimitPositiveList() throws XMLStreamException {
        assertLimit(LIMIT_BARS, "<foo><bar zoo=\"true\" /></foo>", false, true, true, "/foo$", -1, "/foo/bar", 1);
        assertLimit(LIMIT_BARS, "<foo><baz /></foo>", false, true, true, "/foo$", -1, "/foo/baz", 1);
    }

    public void testLimitXMLAttribute() throws XMLStreamException {
        assertLimit(LIMIT_BARS, "<foo><bar zoo=\"false\" /><baz /></foo>", false, false, false, "/foo/bar#zoo=true",
                0);
        assertLimit(LIMIT_BARS, "<foo><bar zoo=\"true\" /><bar zoo=\"false\" /><baz /></foo>", false, false, false,
                "/foo/bar#zoo=true", 1);
    }

    public void testLimitXMLAttributeNamespace() throws XMLStreamException {
        final String NS = "<n:foo xmlns:n=\"sjfk\" xmlns=\"myDefault\"><bar n:zoo=\"true\"></bar><bar zoo=\"true\"></bar>"
                + "<bar zoo=\"false\"></bar><baz></baz></n:foo>";
        assertLimit(NS, "<n:foo xmlns:n=\"sjfk\" xmlns=\"myDefault\"><bar zoo=\"false\"></bar><baz></baz></n:foo>",
                false, false, false, "/foo/bar#zoo=true", 0);
    }

    public void testLimitCountPatterns() throws XMLStreamException {
        assertLimit(LIMIT_BARS, "<foo><bar zoo=\"true\"></bar><bar zoo=\"true\"></bar><baz></baz></foo>", true,
                true, false, ".*", 2);
    }

    // Limits on specific field with specific tag
    public void testLimitPerformance() throws IOException, XMLStreamException {
        final String SAMPLE = getSample(9423);
        final int RUNS = 10;
        final Map<Pattern, Integer> limits = new HashMap<Pattern, Integer>();
        limits.put(Pattern.compile("/record/datafield#tag=Z30"), 10);

        Profiler profiler = new Profiler(RUNS);

        String reduced = "";
        for (int run = 0; run < RUNS; run++) {
            reduced = XMLStepper.limitXML(SAMPLE, limits, false, false, false);
            profiler.beat();
        }
        log.info(String.format("Reduced %d blocks @ %dKB to %dKB at %.1f reductions/sec", RUNS,
                SAMPLE.length() / 1024, reduced.length() / 1024, profiler.getBps(false)));
        assertTrue("The reduced XML should contain datafields after the skipped ones",
                reduced.contains("<datafield tag=\"LOC\""));
    }

    public void testLimitException() throws XMLStreamException {
        Map<Pattern, Integer> lims = new HashMap<Pattern, Integer>();
        lims.put(Pattern.compile("/foo/bar"), 1);
        XMLStreamReader in = xmlFactory.createXMLStreamReader(new StringReader("<foo><bar s=\"t\" /><<</foo>"));
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        XMLStreamWriter out = xmlOutFactory.createXMLStreamWriter(os);
        try {
            XMLStepper.limitXML(in, out, lims, true, true, false);
            fail("An XMLStreamException was expected here due to invalid input XML");
        } catch (XMLStreamException e) {
            // Intended
        }
    }

    // Limits in all datafields, counting on unique datafield#tag=value
    public void testLimitPerformanceCountPatterns() throws IOException, XMLStreamException {
        final String SAMPLE = getSample(9423);
        final int RUNS = 10;
        final Map<Pattern, Integer> limits = new HashMap<Pattern, Integer>();
        limits.put(Pattern.compile("/record/datafield#tag=.*"), 10);

        Profiler profiler = new Profiler(RUNS);

        XMLStepper.Limiter limiter = XMLStepper.createLimiter(limits, true, false, false);

        String reduced = "";
        for (int run = 0; run < RUNS; run++) {
            reduced = limiter.limit(SAMPLE);
            profiler.beat();
        }
        log.info(String.format("Reduced %d blocks @ %dKB to %dKB at %.1f reductions/sec", RUNS,
                SAMPLE.length() / 1024, reduced.length() / 1024, profiler.getBps(false)));
        assertTrue("The reduced XML should contain datafields after the skipped ones",
                reduced.contains("<datafield tag=\"LOC\""));
    }

    private String getSample(int repeats) {
        StringBuilder sb = new StringBuilder(10 * 1024 * 1024);
        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
                + "<record xmlns=\"http://www.loc.gov/MARC21/slim\" "
                + "  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
                + "  xmlns:null=\"http://www.loc.gov/MARC21/slim\" "
                + "  schemaLocation=\"http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd\">\n"
                + "  <leader>00000nap  1233400   6543</leader>\n"
                + "  <datafield tag=\"004\" ind1=\"0\" ind2=\"0\">\n" + "    <subfield code=\"r\">n</subfield>\n"
                + "    <subfield code=\"a\">e</subfield>\n" + "  </datafield>\n"
                + "  <datafield tag=\"001\" ind1=\" \" ind2=\" \">\n"
                + "    <subfield code=\"a\">4106186</subfield>\n" + "    <subfield code=\"f\">a</subfield>\n"
                + "  </datafield>\n");
        for (int i = 0; i < repeats; i++) {
            sb.append("<datafield tag=\"Z30\" ind1=\"-\" ind2=\"2\">\n"
                    + "    <subfield code=\"l\">SOL02</subfield>\n"
                    + "    <subfield code=\"8\">20100327</subfield>\n"
                    + "    <subfield code=\"m\">ISSUE</subfield>\n" + "    <subfield code=\"1\">UASB</subfield>\n"
                    + "    <subfield code=\"2\">UASBH</subfield>\n" + "    <subfield code=\"3\">Bom</subfield>\n"
                    + "    <subfield code=\"5\">").append("12345-67").append(i)
                    .append("</subfield>\n" + "    <subfield code=\"a\">2010</subfield>\n"
                            + "    <subfield code=\"b\">1</subfield>\n"
                            + "    <subfield code=\"c\">3456</subfield>\n"
                            + "    <subfield code=\"f\">67</subfield>\n"
                            + "    <subfield code=\"h\">2010 1  6543</subfield>\n"
                            + "    <subfield code=\"i\">20100821</subfield>\n"
                            + "    <subfield code=\"j\">20101025</subfield>\n"
                            + "    <subfield code=\"k\">20100910</subfield>\n" + "  </datafield>\n");
        }
        sb.append("  <datafield tag=\"STS\" ind1=\" \" ind2=\" \">\n" + "    <subfield code=\"a\">67</subfield>\n"
                + "  </datafield>\n" + "  <datafield tag=\"SBL\" ind1=\" \" ind2=\" \">\n"
                + "    <subfield code=\"a\">FOOB</subfield>\n" + "  </datafield>\n"
                + "  <datafield tag=\"LOC\" ind1=\" \" ind2=\" \">\n" + "    <subfield code=\"b\">FOOB</subfield>\n"
                + "    <subfield code=\"c\">AUGHH</subfield>\n" + "    <subfield code=\"h\">MPG</subfield>\n"
                + "    <subfield code=\"o\">ISSUE</subfield>\n" + "  </datafield>\n"
                + "  <datafield tag=\"STS\" ind1=\" \" ind2=\" \">\n" + "    <subfield code=\"a\">67</subfield>\n"
                + "  </datafield>\n" + "</record>");
        return sb.toString();
    }

    private void assertLimit(String input, String expected, boolean countPatterns, boolean onlyElementMatch,
            boolean discardNonMatched, Object... limits) throws XMLStreamException {
        if (!isCollapsing) {
            expected = expected.replaceAll("<([^> ]+)([^>]*) />", "<$1$2></$1>");
        }
        Map<Pattern, Integer> lims = new HashMap<Pattern, Integer>();
        for (int i = 0; i < limits.length; i += 2) {
            lims.put(Pattern.compile((String) limits[i]), (Integer) limits[i + 1]);
        }
        XMLStreamReader in = xmlFactory.createXMLStreamReader(new StringReader(input));
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        XMLStreamWriter out = xmlOutFactory.createXMLStreamWriter(os);
        XMLStepper.limitXML(in, out, lims, countPatterns, onlyElementMatch, discardNonMatched);
        assertEquals("The input should be reduced properly for limits " + Strings.join(limits), expected,
                os.toString());
        assertLimitConvenience(input, expected, countPatterns, onlyElementMatch, discardNonMatched, limits);
        assertLimitPersistent(input, expected, countPatterns, onlyElementMatch, discardNonMatched, limits);
    }

    private void assertLimitConvenience(String input, String expected, boolean countPatterns,
            boolean onlyElementMatch, boolean discardNonMatched, Object... limits) throws XMLStreamException {
        if (!isCollapsing) {
            expected = expected.replaceAll("<([^> ]+)([^>]*) />", "<$1$2></$1>");
        }
        Map<Pattern, Integer> lims = new HashMap<Pattern, Integer>();
        for (int i = 0; i < limits.length; i += 2) {
            lims.put(Pattern.compile((String) limits[i]), (Integer) limits[i + 1]);
        }

        String os = XMLStepper.limitXML(input, lims, countPatterns, onlyElementMatch, discardNonMatched);
        assertEquals("The input should be convenience reduced properly for limits " + Strings.join(limits),
                expected, os);
    }

    private void assertLimitPersistent(String input, String expected, boolean countPatterns,
            boolean onlyElementMatch, boolean discardNonMatched, Object... limits) throws XMLStreamException {
        if (!isCollapsing) {
            expected = expected.replaceAll("<([^> ]+)([^>]*) />", "<$1$2></$1>");
        }
        Map<Pattern, Integer> lims = new HashMap<Pattern, Integer>();
        for (int i = 0; i < limits.length; i += 2) {
            lims.put(Pattern.compile((String) limits[i]), (Integer) limits[i + 1]);
        }

        XMLStepper.Limiter limiter = XMLStepper.createLimiter(lims, countPatterns, onlyElementMatch,
                discardNonMatched);
        String os = limiter.limit(input);
        assertEquals("The input should be convenience reduced properly for limits " + Strings.join(limits),
                expected, os);
    }

    // Sanity check for traversal of sub
    public void testIterateTags() throws Exception {
        XMLStreamReader xml = xmlFactory.createXMLStreamReader(new StringReader(SAMPLE));
        assertTrue("The first 'bar' should be findable", XMLStepper.findTagStart(xml, "bar"));
        xml.next();

        final AtomicInteger count = new AtomicInteger(0);
        XMLStepper.iterateTags(xml, new XMLStepper.Callback() {
            @Override
            public boolean elementStart(XMLStreamReader xml, List<String> tags, String current)
                    throws XMLStreamException {
                count.incrementAndGet();
                return false;
            }
        });
        assertEquals("Only a single content should be visited", 1, count.get());
        assertTrue("The second 'bar' should be findable", XMLStepper.findTagStart(xml, "bar"));
    }

    private final boolean isCollapsing = writerIsCollapsing();

    @SuppressWarnings("CallToPrintStackTrace")
    private synchronized boolean writerIsCollapsing() {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        try {
            XMLStreamWriter out = xmlOutFactory.createXMLStreamWriter(os);
            out.writeStartElement("foo");
            out.writeEndElement();
            out.flush();
            return "<foo />".equals(os.toString());
        } catch (XMLStreamException e) {
            throw new RuntimeException("Unable to determine if XMLStreamWriter collapses empty elements", e);
        }
    }

    public void testPipePositionOnIgnoredFail() throws XMLStreamException {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        XMLStreamWriter out = xmlOutFactory.createXMLStreamWriter(os);
        XMLStreamReader in = xmlFactory.createXMLStreamReader(new StringReader(SAMPLE));
        assertTrue("The first 'bar' should be findable", XMLStepper.findTagStart(in, "bar"));
        XMLStepper.pipeXML(in, out, false); // until first </bar>
        assertEquals(
                "The reader should be positioned at a character tag (newline) but was positioned at "
                        + XMLUtil.eventID2String(in.getEventType()),
                XMLStreamConstants.CHARACTERS, in.getEventType());
    }

    public void testPipe() throws XMLStreamException {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        XMLStreamWriter out = xmlOutFactory.createXMLStreamWriter(os);
        XMLStreamReader in = xmlFactory.createXMLStreamReader(new StringReader(SAMPLE));
        XMLStepper.pipeXML(in, out, false);
        assertEquals("Piped stream should match input stream", SAMPLE, os.toString());
    }

    public void testGetSubXML_DoubleContent() throws XMLStreamException {
        final String XML = "<field><content foo=\"bar\"/><content foo=\"zoo\"/></field>";
        final String EXPECTED = "<content foo=\"bar\"></content><content foo=\"zoo\"></content>";

        XMLStreamReader in = xmlFactory.createXMLStreamReader(new StringReader(XML));
        in.next();
        String piped = XMLStepper.getSubXML(in, false, true);
        assertEquals("The output should contain the inner XML", EXPECTED, piped);
    }

    public void testGetSubXML_NoOuter() throws XMLStreamException {
        XMLStreamReader in = xmlFactory.createXMLStreamReader(new StringReader(OUTER_FULL));
        assertTrue("The first 'foo' should be findable", XMLStepper.findTagStart(in, "foo"));
        String piped = XMLStepper.getSubXML(in, false, true);
        assertEquals("The output should contain the inner XML", OUTER_SNIPPET, piped);
    }

    public void testGetSubXML_Outer() throws XMLStreamException {
        XMLStreamReader in = xmlFactory.createXMLStreamReader(new StringReader(OUTER_FULL));
        assertTrue("The first 'foo' should be findable", XMLStepper.findTagStart(in, "foo"));
        String piped = XMLStepper.getSubXML(in, false, false);
        assertEquals("The output should contain the inner XML", "<foo><bar>bar1</bar><bar>bar2</bar></foo>", piped);
    }

    public void testPipeComments() throws XMLStreamException {
        final String EXPECTED = "<bar xmlns=\"http://www.example.com/bar_ns/\">"
                + "<nam:subsub xmlns:nam=\"http://example.com/subsub_ns\">content1<!-- Sub comment --></nam:subsub>"
                + "<!-- Comment --></bar>";
        XMLStreamReader in = xmlFactory.createXMLStreamReader(new StringReader(SAMPLE));
        assertTrue("The first 'bar' should be findable", XMLStepper.findTagStart(in, "bar"));
        assertPipe(EXPECTED, in);
    }

    public void testExtract() throws XMLStreamException {
        final String EXPECTED = "<bar xmlns=\"http://www.example.com/bar_ns/\">"
                + "<nam:subsub xmlns:nam=\"http://example.com/subsub_ns\">content1<!-- Sub comment --></nam:subsub>"
                + "<!-- Comment --></bar>";
        XMLStreamReader in = xmlFactory.createXMLStreamReader(new StringReader(SAMPLE));
        assertTrue("The first 'bar' should be findable", XMLStepper.findTagStart(in, "bar"));
        assertEquals(EXPECTED, XMLStepper.getSubXML(in, true));
    }

    // Currently there is no namespace repair functionality
    public void disabletestPipeNamespace() throws XMLStreamException {
        final String EXPECTED = "<bar xmlns=\"http://www.example.com/foo_ns/\">simple bar</bar>";
        XMLStreamReader in = xmlFactory.createXMLStreamReader(new StringReader(DERIVED_NAMESPACE));
        assertTrue("The first 'bar' should be findable", XMLStepper.findTagStart(in, "bar"));
        assertPipe(EXPECTED, in);
    }

    private void assertPipe(String expected, XMLStreamReader xml) throws XMLStreamException {
        String result = XMLStepper.getSubXML(xml, false);
        log.info("Sub-XML: " + result);
        assertEquals("The piper should reproduce the desired sub section of the XML", expected, result);
    }
}