/*******************************************************************************
 * Copyright (c) 1998, 2016 Oracle and/or its affiliates. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
 * which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at 
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     Oracle - initial API and implementation from Oracle TopLink
 ******************************************************************************/  
package org.eclipse.persistence.internal.oxm;

import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Vector;

import org.eclipse.persistence.platform.xml.XMLNamespaceResolver;
import org.eclipse.persistence.platform.xml.XMLPlatformFactory;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;

/**
 * <p >It is common for an XML document to include one or more namespaces.
 * TopLink supports this using its NamespaceResolver. The namespace resolver maintains
 * pairs of namespace prefixes and URIs. TopLink uses these prefixes in conjunction with the
 * XPath statements you specify on EIS mappings to XML records and XML mappings.
 *
 * <p>Although TopLink captures namespace prefixes in the XPath statements for mappings (if applicable),
 * the input document is not required to use the same namespace prefixes. TopLink will use the namespace
 * prefixes specified in the mapping when creating new documents.
 *
 * <p><em>Code Sample</em><br>
 * <code>
 *  <b>NamespaceResolver resolver = new NamespaceResolver();<br>
 *  resolver.put(    "ns", "urn:namespace-example");<br><br></b>
 *
 *  XMLDescriptor descriptor = new XMLDescriptor();<br>
 *  descriptor.setJavaClass(Customer.class); <br>
 *  descriptor.setDefaultRootElement("<b>ns</b>:customer");<br>
 *  descriptor.setNamespaceResolver(resolver);<br><br>
 *
 *  XMLDirectMapping mapping = new XMLDirectMapping();<br>
 *  mapping.setAttributeName("id");<br>
 *  mapping.setXPath("<b>ns</b>:id/text()");<br>
 *  descriptor.addMapping(mapping);
 *  </code>
 *
 *  @see org.eclipse.persistence.oxm.XMLDescriptor
 *  @see org.eclipse.persistence.eis.EISDescriptor
 *
 */
public class NamespaceResolver implements XMLNamespaceResolver {
    private static final String BASE_PREFIX = "ns";

    private String defaultNamespaceURI;
    private Map<String, String> prefixesToNamespaces;
    int prefixCounter;
    private Node dom;

    /**
    * Default constructor, creates a new NamespaceResolver.
     */
    public NamespaceResolver() {
        super();
    }

    /**
     * Copy Constructor
     * @since EclipseLink 2.5.0
     */
    public NamespaceResolver(NamespaceResolver namespaceResolver) {
        this.defaultNamespaceURI = namespaceResolver.defaultNamespaceURI;
        Map<String, String> namespaceResolverPrefixesToNamespaces = namespaceResolver.prefixesToNamespaces;
        if(namespaceResolverPrefixesToNamespaces != null) {
            this.prefixesToNamespaces = new LinkedHashMap<String, String>(namespaceResolverPrefixesToNamespaces.size());
            this.prefixesToNamespaces.putAll(namespaceResolver.prefixesToNamespaces);
        }
        this.prefixCounter = namespaceResolver.prefixCounter;
        this.dom = namespaceResolver.dom;
    }

    public Map<String, String> getPrefixesToNamespaces() {
        if(null == prefixesToNamespaces) {
            prefixesToNamespaces = new LinkedHashMap<String, String>();
        }
        return prefixesToNamespaces;
    }

    public boolean hasPrefixesToNamespaces() {
         return null != prefixesToNamespaces;
    }

    /** Indicates whether given {@code prefix} is assigned to a name-space.
     *
     * @param prefix name-space prefix
     * @return {@code true} if {@code prefix} is present in prefix to name-space map
     *         ({@link #getPrefixesToNamespaces()}
     */
    public boolean hasPrefix(String prefix) {
        return null != prefixesToNamespaces ? prefixesToNamespaces.containsKey(prefix) : false;
    }

    public void setDOM(Node dom) {
        this.dom = dom;
    }

    /**
    * Returns the namespace URI associated with a specified namespace prefix
     *
     * @param  prefix The prefix to lookup a namespace URI for
     * @return The namespace URI associated with the specified prefix
     */
    public String resolveNamespacePrefix(String prefix) {
        if (null == prefix || prefix.length() == 0) {
            return defaultNamespaceURI;
        }
        String uri = null;
        if(null != prefixesToNamespaces) {
            uri = prefixesToNamespaces.get(prefix);
        }
        if(null != uri) {
            return uri;
        } else if (javax.xml.XMLConstants.XML_NS_PREFIX.equals(prefix)) {
            return javax.xml.XMLConstants.XML_NS_URI;
        } else if (javax.xml.XMLConstants.XMLNS_ATTRIBUTE.equals(prefix)) {
            return javax.xml.XMLConstants.XMLNS_ATTRIBUTE_NS_URI;
        }
        if(dom != null) {
            return XMLPlatformFactory.getInstance().getXMLPlatform().resolveNamespacePrefix(dom, prefix);
        }
        return null;
    }

    /**
     * Return the namespace prefix associated with a namespace URI.
     * @param uri A namespace URI.
     * @return The prefix associated with the namespace URI.
     */
    public String resolveNamespaceURI(String uri) {
        if(null == uri) {
            return null;
        }
        if(null != prefixesToNamespaces) {
            for(Entry<String, String> entry : prefixesToNamespaces.entrySet()) {
                if(uri.equals(entry.getValue())) {
                    return entry.getKey();
                }
            }
        }
        if (uri.equalsIgnoreCase(javax.xml.XMLConstants.XMLNS_ATTRIBUTE_NS_URI)) {
            return javax.xml.XMLConstants.XMLNS_ATTRIBUTE;
        } else if (uri.equalsIgnoreCase(javax.xml.XMLConstants.XML_NS_URI)) {
            return javax.xml.XMLConstants.XML_NS_PREFIX;
        }
        return resolveNamespaceURI(dom, uri);
    }

    private String resolveNamespaceURI(Node node, String uri) {
        if(null == node) {
            return null;
        }

        // If the element is of the same namespace URI, then return the prefix.
        if(uri.equals(node.getNamespaceURI())) {
            return node.getPrefix();
        } 

        // Check the namespace URI declarations.
        NamedNodeMap namedNodeMap = node.getAttributes();
        if(null != namedNodeMap) {
            int namedNodeMapSize = namedNodeMap.getLength();
            for(int x=0; x<namedNodeMapSize; x++) {
                Node attr = namedNodeMap.item(x);
                if(javax.xml.XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(attr.getNamespaceURI())) {
                    if(uri.equals(attr.getNodeValue())) {
                        if(attr.getLocalName() != null && (!(attr.getLocalName().equals(javax.xml.XMLConstants.XMLNS_ATTRIBUTE)))) {
                            return attr.getLocalName();
                        } else {
                            return "";
                        }
                    }
                }
            }
        }

        // Repeat the process on the parent node.
        return resolveNamespaceURI(node.getParentNode(), uri);
    }

    /**
     * Adds a namespace to the collection of namespaces on the NamespaceResolver
     *
     * @param  prefix  The prefix for a namespace
     * @param  namespaceURI  The namespace URI associated with the specified prefix
    */
    public void put(String prefix, String namespaceURI) {
        if(null == prefix || 0 == prefix.length()) {
            defaultNamespaceURI = namespaceURI;
        } else {
            //Replace same namespace with given prefix and put them to the end of list.

            //If you have prefix xmlns:oxm="namespace1" defined on the schema root,
            //and you (programmatically via namespace resolver) inject prefix xmlns:myns="namespace1" on some element (more deeply)
            //in the schema, you want this element (in the xml instance) to be prefixed with this myns (because it was defined more closely to the given element).
            //This can probably be setup in different way (declaratively maybe with some JAXB spec support or MOXy external xml mechanism).
            //This behavior is preserved, but it is now working independently on the JDK (because we are using HashMap and we are changing the order
            //of items in the LinkedHashMap so the resolver always find the prefix which is more closely (in xml schema) to the given element.
            ///@see XMLRootComplexDifferentPrefixTestCases
            Map<String, String> removedItems = null;
            if (getPrefixesToNamespaces().containsValue(namespaceURI.intern())) {
                removedItems = new LinkedHashMap<String, String>();
                for (Map.Entry<String, String> prefixEntry : getPrefixesToNamespaces().entrySet()) {
                    if (namespaceURI.intern().equals(prefixEntry.getValue())) {
                        removedItems.put(prefixEntry.getKey(), prefixEntry.getValue());
                    }
                }
            }
            if (null != removedItems) {
                for (String key : removedItems.keySet()) {
                    getPrefixesToNamespaces().remove(key);
                }
            }
            getPrefixesToNamespaces().put(prefix, namespaceURI.intern());
            if (null != removedItems) {
                for (Map.Entry<String, String> removedEntry : removedItems.entrySet()) {
                    getPrefixesToNamespaces().put(removedEntry.getKey(), removedEntry.getValue());
                }
            }
        }
    }

    /**
    * Returns the list of prefixes in the NamespaceResolver
     *
     * @return An Enumeration containing the prefixes in the NamespaceResolver
     */
    public Enumeration getPrefixes() {
        if(hasPrefixesToNamespaces()) {
            return new IteratorEnumeration(getPrefixesToNamespaces().keySet().iterator());
        } else {
            return new IteratorEnumeration(null);
        }
    }

    /**
    * INTERNAL:
     * Returns a Vector of of Namespace objects in the current Namespace Resolver
     * Used for deployment XML
     * @return  A Vector containing the namespace URIs in the namespace resolver
     */
    public Vector getNamespaces() {
        if(!hasPrefixesToNamespaces()) {
            return new Vector(0);
        }
        Vector names = new Vector(prefixesToNamespaces.size());
        for(Entry<String, String> entry: prefixesToNamespaces.entrySet()) {
            Namespace namespace = new Namespace(entry.getKey(), entry.getValue());
            names.addElement(namespace);
        }
        return names;
    }

    /**
    * INTERNAL:
     * Set the namespaces on the namespace resolver based on the specified Vector of Namespace objects
     * Used for deployment XML
     * @param  names A Vector of namespace URIs
     */
    public void setNamespaces(Vector names) {
        prefixesToNamespaces = new LinkedHashMap<String, String>(names.size());
        for(Namespace namespace : (Vector<Namespace>) names) {
            if ((namespace.getPrefix() != null) && (namespace.getNamespaceURI() != null)) {
                prefixesToNamespaces.put(namespace.getPrefix(), namespace.getNamespaceURI());
            }
        }
    }

    public String generatePrefix() {
        return generatePrefix(getNextPrefix());
    }

    private String getNextPrefix() {
        return BASE_PREFIX + prefixCounter++;
    }

    public String generatePrefix(String defaultPrefix) {
        String lookup = resolveNamespacePrefix(defaultPrefix);
        while (lookup != null) {
            defaultPrefix = getNextPrefix();
            lookup = resolveNamespacePrefix(defaultPrefix);
        }
        return defaultPrefix;
    }

    public void removeNamespace(String prefix) {
        if(null != prefixesToNamespaces) {
            prefixesToNamespaces.remove(prefix);
        }
    }

    public void setDefaultNamespaceURI(String namespaceUri) {    	        
        if(namespaceUri == null){
        	defaultNamespaceURI = namespaceUri;        	
    	}else{
    		defaultNamespaceURI = namespaceUri.intern();
    	}
    }

    public String getDefaultNamespaceURI() {
        if(null != defaultNamespaceURI) {
            return defaultNamespaceURI;
        } else if(dom != null) {
            return XMLPlatformFactory.getInstance().getXMLPlatform().resolveNamespacePrefix(dom, null);
        }
        return null;
    }

    private static class IteratorEnumeration implements Enumeration {

        private Iterator iterator;

        public IteratorEnumeration(Iterator iterator) {
            this.iterator = iterator;
        }

        public boolean hasMoreElements() {
            if(null == iterator) {
                return false;
            }
            return iterator.hasNext();
        }

        public Object nextElement() {
            return iterator.next();
        }

    }

}
