TestMergedNsContext.java

package wstxtest.evt;

import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.stream.events.Namespace;

import com.ctc.wstx.evt.MergedNsContext;
import com.ctc.wstx.util.BaseNsContext;
import org.junit.jupiter.api.Test;


/**
 * Unit tests for {@link MergedNsContext}, the hierarchical
 * {@link NamespaceContext} used by the event-construction API.
 */
public class TestMergedNsContext extends wstxtest.BaseJUnit4Test
{
    private final XMLEventFactory eventFactory = XMLEventFactory.newInstance();

    // ---------- basic prefix/URI lookup ----------

    @Test
    public void testLookupNullParent()
    {
        BaseNsContext ctxt = MergedNsContext.construct(null, ns("a", "uri-a", "b", "uri-b"));
        assertEquals("uri-a", ctxt.getNamespaceURI("a"));
        assertEquals("uri-b", ctxt.getNamespaceURI("b"));
        // unknown prefix -> null
        assertNull(ctxt.getNamespaceURI("nope"));

        assertEquals("a", ctxt.getPrefix("uri-a"));
        assertEquals("b", ctxt.getPrefix("uri-b"));
        assertNull(ctxt.getPrefix("uri-missing"));
    }

    @Test
    public void testEmptyLocalListWithNullParent()
    {
        // construct(null, null) should be safe and yield an empty context
        BaseNsContext ctxt = MergedNsContext.construct(null, null);
        assertNull(ctxt.getNamespaceURI("anything"));
        assertNull(ctxt.getPrefix("uri-x"));
        assertFalse(ctxt.getPrefixes("uri-x").hasNext());
        assertFalse(ctxt.getNamespaces().hasNext());
    }

    @Test
    public void testDefaultNamespace()
    {
        // Default namespace is represented by empty-string prefix
        BaseNsContext ctxt = MergedNsContext.construct(
                null, Collections.singletonList(eventFactory.createNamespace("default-uri")));
        assertEquals("default-uri", ctxt.getNamespaceURI(""));
        assertEquals("", ctxt.getPrefix("default-uri"));
    }

    // ---------- parent fallback ----------

    @Test
    public void testParentFallbackForUnknownPrefix()
    {
        BaseNsContext parent = MergedNsContext.construct(null, ns("p", "parent-uri"));
        BaseNsContext child = MergedNsContext.construct(parent, ns("c", "child-uri"));

        // Local wins
        assertEquals("child-uri", child.getNamespaceURI("c"));
        // Unknown locally -> delegate to parent
        assertEquals("parent-uri", child.getNamespaceURI("p"));
        // Same for prefix lookup
        assertEquals("p", child.getPrefix("parent-uri"));
        assertEquals("c", child.getPrefix("child-uri"));
    }

    @Test
    public void testGetPrefixesCombinesLocalAndParent()
    {
        // Same URI mapped to two different prefixes ��� local AND parent
        BaseNsContext parent = MergedNsContext.construct(null, ns("p1", "shared-uri"));
        BaseNsContext child = MergedNsContext.construct(parent,
                ns("p2", "shared-uri", "other", "other-uri"));

        Set<String> prefixes = collect(child.getPrefixes("shared-uri"));
        assertEquals(new HashSet<>(Arrays.asList("p1", "p2")), prefixes);
    }

    @Test
    public void testChildPrefixShadowsParent()
    {
        // When the SAME prefix is declared in both child and parent, child wins
        // for prefix->URI lookup ��� the most important shadowing semantic.
        BaseNsContext parent = MergedNsContext.construct(null, ns("x", "parent-uri"));
        BaseNsContext child  = MergedNsContext.construct(parent, ns("x", "child-uri"));

        assertEquals("child-uri", child.getNamespaceURI("x"));
        assertEquals("x", child.getPrefix("child-uri"));
    }

    @Test
    public void testGetPrefixesOnlyInParent()
    {
        BaseNsContext parent = MergedNsContext.construct(null, ns("only", "parent-only"));
        BaseNsContext child = MergedNsContext.construct(parent, ns("c", "child-uri"));

        Set<String> prefixes = collect(child.getPrefixes("parent-only"));
        assertEquals(Collections.singleton("only"), prefixes);
    }

    @Test
    public void testGetPrefixesUnknown()
    {
        BaseNsContext ctxt = MergedNsContext.construct(null, ns("a", "uri-a"));
        assertFalse(ctxt.getPrefixes("nothing").hasNext());
    }

    // ---------- predefined prefixes handled by BaseNsContext ----------

    @Test
    public void testPredefinedXmlPrefix()
    {
        BaseNsContext ctxt = MergedNsContext.construct(null, ns("a", "uri-a"));
        assertEquals(XMLConstants.XML_NS_URI,
                ctxt.getNamespaceURI(XMLConstants.XML_NS_PREFIX));
        assertEquals(XMLConstants.XMLNS_ATTRIBUTE_NS_URI,
                ctxt.getNamespaceURI(XMLConstants.XMLNS_ATTRIBUTE));

        assertEquals(XMLConstants.XML_NS_PREFIX,
                ctxt.getPrefix(XMLConstants.XML_NS_URI));
        assertEquals(XMLConstants.XMLNS_ATTRIBUTE,
                ctxt.getPrefix(XMLConstants.XMLNS_ATTRIBUTE_NS_URI));
    }

    @Test
    public void testNullPrefixThrows()
    {
        BaseNsContext ctxt = MergedNsContext.construct(null, ns("a", "uri-a"));
        try {
            ctxt.getNamespaceURI(null);
            fail("Expected IllegalArgumentException for null prefix");
        } catch (IllegalArgumentException e) {
            // expected
        }
    }

    @Test
    public void testNullOrEmptyUriThrows()
    {
        BaseNsContext ctxt = MergedNsContext.construct(null, ns("a", "uri-a"));
        try {
            ctxt.getPrefix(null);
            fail("Expected IllegalArgumentException for null URI");
        } catch (IllegalArgumentException e) { /* expected */ }
        try {
            ctxt.getPrefix("");
            fail("Expected IllegalArgumentException for empty URI");
        } catch (IllegalArgumentException e) { /* expected */ }
        try {
            ctxt.getPrefixes(null);
            fail("Expected IllegalArgumentException for null URI");
        } catch (IllegalArgumentException e) { /* expected */ }
    }

    // ---------- getNamespaces() ----------

    @Test
    public void testGetNamespacesReturnsLocalOnly()
    {
        BaseNsContext parent = MergedNsContext.construct(null, ns("p", "parent-uri"));
        BaseNsContext child = MergedNsContext.construct(parent, ns("c", "child-uri"));

        Iterator<Namespace> it = child.getNamespaces();
        assertTrue(it.hasNext());
        Namespace ns = it.next();
        assertEquals("c", ns.getPrefix());
        assertEquals("child-uri", ns.getNamespaceURI());
        assertFalse(it.hasNext());
    }

    // ---------- outputNamespaceDeclarations(Writer) ----------

    @Test
    public void testOutputNamespaceDeclarationsToWriter() throws IOException
    {
        // Mix of default namespace and a prefixed one to cover both branches
        List<Namespace> nsList = new ArrayList<>();
        nsList.add(eventFactory.createNamespace("default-uri"));
        nsList.add(eventFactory.createNamespace("p", "prefix-uri"));
        BaseNsContext ctxt = MergedNsContext.construct(null, nsList);

        StringWriter sw = new StringWriter();
        ctxt.outputNamespaceDeclarations(sw);
        String out = sw.toString();

        // Default namespace declaration (no ":prefix" segment)
        assertTrue("expected default xmlns declaration in '" + out + "'",
                out.contains(" xmlns=\"default-uri\""));
        // Prefixed namespace declaration
        assertTrue("expected prefixed xmlns declaration in '" + out + "'",
                out.contains(" xmlns:p=\"prefix-uri\""));
    }

    // ---------- outputNamespaceDeclarations(XMLStreamWriter) ----------

    @Test
    public void testOutputNamespaceDeclarationsToStreamWriter() throws Exception
    {
        List<Namespace> nsList = new ArrayList<>();
        nsList.add(eventFactory.createNamespace("default-uri"));
        nsList.add(eventFactory.createNamespace("p", "prefix-uri"));
        BaseNsContext ctxt = MergedNsContext.construct(null, nsList);

        // Use a recording stub so writeDefaultNamespace/writeNamespace are
        // observed directly without entanglement in real-writer state tracking.
        RecordingStreamWriter rec = new RecordingStreamWriter();
        ctxt.outputNamespaceDeclarations(rec);

        assertEquals(Arrays.asList(
                "default:default-uri",
                "prefixed:p=prefix-uri"), rec.calls);
    }

    // ---------- helpers ----------

    /** Build a Namespace list from prefix/uri pairs. */
    private List<Namespace> ns(String... pairs)
    {
        if ((pairs.length & 1) != 0) {
            throw new IllegalArgumentException("Need even number of args");
        }
        List<Namespace> list = new ArrayList<>(pairs.length / 2);
        for (int i = 0; i < pairs.length; i += 2) {
            list.add(eventFactory.createNamespace(pairs[i], pairs[i + 1]));
        }
        return list;
    }

    private static Set<String> collect(Iterator<String> it)
    {
        Set<String> s = new HashSet<>();
        while (it.hasNext()) {
            s.add(it.next());
        }
        return s;
    }

    /**
     * Minimal {@link XMLStreamWriter} stub that records only the namespace
     * declaration calls exercised by {@link MergedNsContext#outputNamespaceDeclarations(XMLStreamWriter)}.
     */
    static final class RecordingStreamWriter implements XMLStreamWriter
    {
        final List<String> calls = new ArrayList<>();

        @Override public void writeDefaultNamespace(String uri) {
            calls.add("default:" + uri);
        }
        @Override public void writeNamespace(String prefix, String uri) {
            calls.add("prefixed:" + prefix + "=" + uri);
        }

        // ----- everything else is unused by the method under test -----
        @Override public void writeStartElement(String localName) { unsupported(); }
        @Override public void writeStartElement(String nsURI, String localName) { unsupported(); }
        @Override public void writeStartElement(String prefix, String localName, String nsURI) { unsupported(); }
        @Override public void writeEmptyElement(String nsURI, String localName) { unsupported(); }
        @Override public void writeEmptyElement(String prefix, String localName, String nsURI) { unsupported(); }
        @Override public void writeEmptyElement(String localName) { unsupported(); }
        @Override public void writeEndElement() { unsupported(); }
        @Override public void writeEndDocument() { unsupported(); }
        @Override public void close() { /* no-op */ }
        @Override public void flush() { /* no-op */ }
        @Override public void writeAttribute(String localName, String value) { unsupported(); }
        @Override public void writeAttribute(String prefix, String nsURI, String localName, String value) { unsupported(); }
        @Override public void writeAttribute(String nsURI, String localName, String value) { unsupported(); }
        @Override public void writeComment(String data) { unsupported(); }
        @Override public void writeProcessingInstruction(String target) { unsupported(); }
        @Override public void writeProcessingInstruction(String target, String data) { unsupported(); }
        @Override public void writeCData(String data) { unsupported(); }
        @Override public void writeDTD(String dtd) { unsupported(); }
        @Override public void writeEntityRef(String name) { unsupported(); }
        @Override public void writeStartDocument() { unsupported(); }
        @Override public void writeStartDocument(String version) { unsupported(); }
        @Override public void writeStartDocument(String encoding, String version) { unsupported(); }
        @Override public void writeCharacters(String text) { unsupported(); }
        @Override public void writeCharacters(char[] text, int start, int len) { unsupported(); }
        @Override public String getPrefix(String uri) { return null; }
        @Override public void setPrefix(String prefix, String uri) { /* no-op */ }
        @Override public void setDefaultNamespace(String uri) { /* no-op */ }
        @Override public void setNamespaceContext(NamespaceContext ctx) { /* no-op */ }
        @Override public NamespaceContext getNamespaceContext() { return null; }
        @Override public Object getProperty(String name) { return null; }

        private void unsupported() {
            throw new UnsupportedOperationException("Not used by test");
        }
    }
}