NamespaceContextImpl.java

/*
 * Copyright (c) 1997, 2022 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0, which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

package org.glassfish.jaxb.runtime.v2.runtime.output;

import com.sun.istack.NotNull;
import com.sun.istack.Nullable;
import org.glassfish.jaxb.runtime.marshaller.NamespacePrefixMapper;
import org.glassfish.jaxb.core.v2.WellKnownNamespace;
import org.glassfish.jaxb.runtime.v2.runtime.Name;
import org.glassfish.jaxb.runtime.v2.runtime.NamespaceContext2;
import org.glassfish.jaxb.runtime.v2.runtime.XMLSerializer;
import org.xml.sax.SAXException;

import javax.xml.XMLConstants;
import javax.xml.stream.XMLStreamException;
import java.io.IOException;
import java.util.Collections;
import java.util.Iterator;

/**
 * Keeps track of in-scope namespace bindings for the marshaller.
 *
 * <p>
 * This class is also used to keep track of tag names for each element
 * for the marshaller (for the performance reason.)
 *
 * @author Kohsuke Kawaguchi
 */
public final class NamespaceContextImpl implements NamespaceContext2 {
    private final XMLSerializer owner;

    private String[] prefixes = new String[4];
    private String[] nsUris = new String[4];
//    /**
//     * True if the correponding namespace declaration is an authentic one that should be printed.
//     *
//     * False if it's a re-discovered in-scope namespace binding available at the ancestor elements
//     * outside this marhsalling. The false value is used to incorporate the in-scope namespace binding
//     * information from {@link #inscopeNamespaceContext}. When false, such a declaration does not need
//     * to be printed, as it's already available in ancestors.
//     */
//    private boolean[] visible = new boolean[4];
//
//    /**
//     * {@link NamespaceContext} that informs this {@link XMLSerializer} about the
//     * in-scope namespace bindings of the ancestor elements outside this marshalling.
//     *
//     * <p>
//     * This is used when the marshaller is marshalling into a subtree that has ancestor
//     * elements created outside the JAXB marshaller.
//     *
//     * Its {@link NamespaceContext#getPrefix(String)} is used to discover in-scope namespace
//     * binding,
//     */
//    private final NamespaceContext inscopeNamespaceContext;

    /**
     * Number of URIs declared. Identifies the valid portion of
     * the {@link #prefixes} and {@link #nsUris} arrays.
     */
    private int size;

    private Element current;

    /**
     * This is the {@link Element} whose prev==null.
     * This element is used to hold the contextual namespace bindings
     * that are assumed to be outside of the document we are marshalling.
     * Specifically the xml prefix and any other user-specified bindings.
     *
     * @see NamespacePrefixMapper#getPreDeclaredNamespaceUris()
     */
    private final Element top;

    /**
     * Never null.
     */
    private NamespacePrefixMapper prefixMapper = defaultNamespacePrefixMapper;

    /**
     * True to allow new URIs to be declared. False otherwise.
     */
    public boolean collectionMode;


    public NamespaceContextImpl(XMLSerializer owner) {
        this.owner = owner;

        current = top = new Element(this,null);
        // register namespace URIs that are implicitly bound
        put(XMLConstants.XML_NS_URI,XMLConstants.XML_NS_PREFIX);
    }

    public void setPrefixMapper( NamespacePrefixMapper mapper ) {
        if(mapper==null)
            mapper = defaultNamespacePrefixMapper;
        this.prefixMapper = mapper;
    }

    public NamespacePrefixMapper getPrefixMapper() {
        return prefixMapper;
    }

    public void reset() {
        current = top;
        size = 1;
        collectionMode = false;
    }

    /**
     * Returns the prefix index to the specified URI.
     * This method allocates a new URI if necessary.
     */
    public int declareNsUri( String uri, String preferedPrefix, boolean requirePrefix ) {
        preferedPrefix = prefixMapper.getPreferredPrefix(uri,preferedPrefix,requirePrefix);

        if(uri.length()==0) {
            for( int i=size-1; i>=0; i-- ) {
                if(nsUris[i].length()==0)
                    return i; // already declared
                if(prefixes[i].length()==0) {
                    // the default prefix is already taken.
                    // move that URI to another prefix, then assign "" to the default prefix.
                    assert current.defaultPrefixIndex==-1 && current.oldDefaultNamespaceUriIndex==-1;

                    String oldUri = nsUris[i];
                    String[] knownURIs = owner.nameList.namespaceURIs;

                    if(current.baseIndex<=i) {
                        // this default prefix is declared in this context. just reassign it

                        nsUris[i] = "";

                        int subst = put(oldUri,null);

                        // update uri->prefix table if necessary
                        for( int j=knownURIs.length-1; j>=0; j-- ) {
                            if(knownURIs[j].equals(oldUri)) {
                                owner.knownUri2prefixIndexMap[j] = subst;
                                break;
                            }
                        }
                        if (current.elementLocalName != null) {
                            current.setTagName(subst, current.elementLocalName, current.getOuterPeer());
                        }
                        return i;
                    } else {
                        // first, if the previous URI assigned to "" is
                        // a "known URI", remember what we've reallocated
                        // so that we can fix it when this context pops.
                        for( int j=knownURIs.length-1; j>=0; j-- ) {
                            if(knownURIs[j].equals(oldUri)) {
                                current.defaultPrefixIndex = i;
                                current.oldDefaultNamespaceUriIndex = j;
                                // assert commented out; too strict/not valid any more
                                // assert owner.knownUri2prefixIndexMap[j]==current.defaultPrefixIndex;
                                // update the table to point to the prefix we'll declare
                                owner.knownUri2prefixIndexMap[j] = size;
                                break;
                            }
                        }
                        if (current.elementLocalName!=null) {
    						current.setTagName(size, current.elementLocalName, current.getOuterPeer());
                        }
                        
                        put(nsUris[i],null);
                        return put("", "");
                    }
                }
            }

            // "" isn't in use
            return put("", "");
        } else {
            // check for the existing binding
            for( int i=size-1; i>=0; i-- ) {
                String p = prefixes[i];
                if(nsUris[i].equals(uri)) {
                    if (!requirePrefix || p.length()>0)
                        return i;
                    // declared but this URI is bound to empty. Look further
                }
                if(p.equals(preferedPrefix)) {
                    // the suggested prefix is already taken. can't use it
                    preferedPrefix = null;
                }
            }

            if(preferedPrefix==null && requirePrefix)
                // we know we can't bind to "", but we don't have any possible name at hand.
                // generate it here to avoid this namespace to be bound to "".
                preferedPrefix = makeUniquePrefix();

            // haven't been declared. allocate a new one
            // if the preferred prefix is already in use, it should have been set to null by this time
            return put(uri, preferedPrefix);
        }
    }

    @Override
    public int force(@NotNull String uri, @NotNull String prefix) {
        // check for the existing binding

        for( int i=size-1; i>=0; i-- ) {
            if(prefixes[i].equals(prefix)) {
                if(nsUris[i].equals(uri))
                    return i;   // found duplicate
                else
                    // the prefix is used for another namespace. we need to declare it
                    break;
            }
        }

        return put(uri, prefix);
    }

    /**
     * Puts this new binding into the declared prefixes list
     * without doing any duplicate check.
     *
     * This can be used to forcibly set namespace declarations.
     *
     * <p>
     * Most of the time {@link #declareNamespace(String, String, boolean)} shall be used.
     *
     * @return
     *      the index of this new binding.
     */
    public int put(@NotNull String uri, @Nullable String prefix) {
        if(size==nsUris.length) {
            // reallocate
            String[] u = new String[nsUris.length*2];
            String[] p = new String[prefixes.length*2];
            System.arraycopy(nsUris,0,u,0,nsUris.length);
            System.arraycopy(prefixes,0,p,0,prefixes.length);
            nsUris = u;
            prefixes = p;
        }
        if(prefix==null) {
            if(size==1)
                prefix = "";    // if this is the first user namespace URI we see, use "".
            else {
                // otherwise make up an unique name
                prefix = makeUniquePrefix();
            }
        }
        nsUris[size] = uri;
        prefixes[size] = prefix;

        return size++;
    }

    private String makeUniquePrefix() {
        String prefix;
        prefix = new StringBuilder(5).append("ns").append(size).toString();
        while(getNamespaceURI(prefix)!=null) {
            prefix += '_';  // under a rare circumstance there might be existing 'nsNNN', so rename them
        }
        return prefix;
    }


    public Element getCurrent() {
        return current;
    }

    /**
     * Returns the prefix index of the specified URI.
     * It is an error if the URI is not declared.
     */
    public int getPrefixIndex( String uri ) {
        for( int i=size-1; i>=0; i-- ) {
                if(nsUris[i].equals(uri))
                    return i;
        }
        throw new IllegalStateException();
    }

    /**
     * Gets the prefix from a prefix index.
     *
     * The behavior is undefined if the index is out of range.
     */
    public String getPrefix(int prefixIndex) {
        return prefixes[prefixIndex];
    }

    public String getNamespaceURI(int prefixIndex) {
        return nsUris[prefixIndex];
    }

    /**
     * Gets the namespace URI that is bound to the specified prefix.
     *
     * @return null
     *      if the prefix is unbound.
     */
    @Override
    public String getNamespaceURI(String prefix) {
        for( int i=size-1; i>=0; i-- )
            if(prefixes[i].equals(prefix))
                return nsUris[i];
        return null;
    }

    /**
     * Returns the prefix of the specified URI,
     * or null if none exists.
     */
    @Override
    public String getPrefix( String uri ) {
        if(collectionMode) {
            return declareNamespace(uri,null,false);
        } else {
            for( int i=size-1; i>=0; i-- )
                if(nsUris[i].equals(uri))
                    return prefixes[i];
            return null;
        }
    }

    @Override
    public Iterator<String> getPrefixes(String uri) {
        String prefix = getPrefix(uri);
        if(prefix==null)
            return Collections.emptyIterator();
        else
            return Collections.singleton(uri).iterator();
    }

    @Override
    public String declareNamespace(String namespaceUri, String preferedPrefix, boolean requirePrefix) {
        int idx = declareNsUri(namespaceUri,preferedPrefix,requirePrefix);
        return getPrefix(idx);
    }

    /**
     * Number of total bindings declared.
     */
    public int count() {
        return size;
    }


    /**
     * This model of namespace declarations maintain the following invariants.
     *
     * <ul>
     *  <li>If a non-empty prefix is declared, it will never be reassigned to different namespace URIs.</li>
     * </ul>
     */
    public final class Element {

        public final NamespaceContextImpl context;

        /**
         * {@link Element}s form a doubly-linked list.
         */
        private final Element prev;
        private Element next;

        private int oldDefaultNamespaceUriIndex;
        private int defaultPrefixIndex;


        /**
         * The numbe of prefixes declared by ancestor {@link Element}s.
         */
        private int baseIndex;

        /**
         * The depth of the {@link Element}.
         *
         * This value is equivalent as the result of the following computation.
         *
         * <pre>
         * int depth() {
         *   int i=-1;
         *   for(Element e=this; e!=null;e=e.prev)
         *     i++;
         *   return i;
         * }
         * </pre>
         */
        private final int depth;



        private int elementNamePrefix;
        private String elementLocalName;

        /**
         * Tag name of this element.
         * Either this field is used or the {@link #elementNamePrefix} and {@link #elementLocalName} pair.
         */
        private Name elementName;

        /**
         * Used for the binder. The JAXB object that corresponds to this element.
         */
        private Object outerPeer;
        private Object innerPeer;


        private Element(NamespaceContextImpl context,Element prev) {
            this.context = context;
            this.prev = prev;
            this.depth = (prev==null) ? 0 : prev.depth+1;
        }

        /**
         * Returns true if this  represents the root element that
         * we are marshalling.
         */
        public boolean isRootElement() {
            return depth==1;
        }

        public Element push() {
            if(next==null)
                next = new Element(context,this);
            next.onPushed();
            return next;
        }

        public Element pop() {
            if(oldDefaultNamespaceUriIndex>=0) {
                // restore the old default namespace URI binding
                context.owner.knownUri2prefixIndexMap[oldDefaultNamespaceUriIndex] = defaultPrefixIndex;
            }
            context.size = baseIndex;
            context.current = prev;
            // release references to user objects
            outerPeer = innerPeer = null;
            return prev;
        }

        private void onPushed() {
            oldDefaultNamespaceUriIndex = defaultPrefixIndex = -1;
            baseIndex = context.size;
            context.current = this;
        }

        public void setTagName( int prefix, String localName, Object outerPeer ) {
            assert localName!=null;
            this.elementNamePrefix = prefix;
            this.elementLocalName = localName;
            this.elementName = null;
            this.outerPeer = outerPeer;
        }

        public void setTagName( Name tagName, Object outerPeer ) {
            assert tagName!=null;
            this.elementName = tagName;
            this.outerPeer = outerPeer;
        }

        public void startElement(XmlOutput out, Object innerPeer) throws IOException, XMLStreamException {
            this.innerPeer = innerPeer;
            if(elementName!=null) {
                out.beginStartTag(elementName);
            } else {
                out.beginStartTag(elementNamePrefix,elementLocalName);
            }
        }

        public void endElement(XmlOutput out) throws IOException, SAXException, XMLStreamException {
            if(elementName!=null) {
                out.endTag(elementName);
                elementName = null;
            } else {
                out.endTag(elementNamePrefix,elementLocalName);
            }
        }

        /**
         * Gets the number of bindings declared on this element.
         */
        public int count() {
            return context.size-baseIndex;
        }

        /**
         * Gets the prefix declared in this context.
         *
         * @param idx
         *      between 0 and {@link #count()}
         */
        public String getPrefix(int idx) {
            return context.prefixes[baseIndex+idx];
        }

        /**
         * Gets the namespace URI declared in this context.
         *
         * @param idx
         *      between 0 and {@link #count()}
         */
        public String getNsUri(int idx) {
            return context.nsUris[baseIndex+idx];
        }

        public int getBase() {
            return baseIndex;
        }

        public Object getOuterPeer() {
            return outerPeer;
        }

        public Object getInnerPeer() {
            return innerPeer;
        }

        /**
         * Gets the parent .
         */
        public Element getParent() {
            return prev;
        }
    }


    /**
     * Default {@link NamespacePrefixMapper} implementation used when
     * it is not specified by the user.
     */
    private static final NamespacePrefixMapper defaultNamespacePrefixMapper = new NamespacePrefixMapper() {
        @Override
        public String getPreferredPrefix(String namespaceUri, String suggestion, boolean requirePrefix) {
            if( XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI.equals(namespaceUri) )
                return "xsi";
            if( XMLConstants.W3C_XML_SCHEMA_NS_URI.equals(namespaceUri) )
                return "xs";
            if( namespaceUri.equals(WellKnownNamespace.XML_MIME_URI) )
                return "xmime";
            return suggestion;
        }
    };
}