/*
 * ModeShape (http://www.modeshape.org)
 *
 * 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 org.modeshape.jcr;

import java.security.AccessControlException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.jcr.AccessDeniedException;
import javax.jcr.NamespaceException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.xml.XMLConstants;
import org.modeshape.common.annotation.NotThreadSafe;
import org.modeshape.common.util.CheckArg;
import org.modeshape.common.xml.XmlCharacters;
import org.modeshape.jcr.value.Name;
import org.modeshape.jcr.value.NamespaceRegistry;
import org.modeshape.jcr.value.NamespaceRegistry.Namespace;
import org.modeshape.jcr.value.ValueFormatException;

/**
 * A thread-safe JCR {@link javax.jcr.NamespaceRegistry} implementation that has the standard JCR namespaces pre-registered and
 * enforces the JCR semantics for {@link #registerNamespace(String, String) registering} and {@link #unregisterNamespace(String)
 * unregistering} namespaces.
 * <p>
 * Note that this implementation is {@link NotThreadSafe not thread safe}, since it is used within a single {@link JcrWorkspace}
 * and single {@link JcrSession}, and according to the JCR specification these interfaces are not thread safe.
 * </p>
 */
@NotThreadSafe
class JcrNamespaceRegistry implements org.modeshape.jcr.api.NamespaceRegistry {

    public static enum Behavior {
        SESSION,
        WORKSPACE;
    }

    static final String DEFAULT_NAMESPACE_PREFIX = "";
    static final String DEFAULT_NAMESPACE_URI = "";

    static final String XML_NAMESPACE_PREFIX = XMLConstants.XML_NS_PREFIX;
    static final String XML_NAMESPACE_URI = XMLConstants.XML_NS_URI;
    static final String XMLNS_NAMESPACE_PREFIX = XMLConstants.XMLNS_ATTRIBUTE;
    static final String XMLNS_NAMESPACE_URI = XMLConstants.XMLNS_ATTRIBUTE_NS_URI;

    static final String XML_SCHEMA_NAMESPACE_PREFIX = "xs";
    static final String XML_SCHEMA_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema";
    static final String XML_SCHEMA_INSTANCE_NAMESPACE_PREFIX = "xsi";
    static final String XML_SCHEMA_INSTANCE_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema-instance";

    static final Set<String> STANDARD_BUILT_IN_PREFIXES;
    static final Set<String> STANDARD_BUILT_IN_URIS;
    static final Map<String, String> STANDARD_BUILT_IN_NAMESPACES_BY_PREFIX;
    static final Map<String, String> STANDARD_BUILT_IN_PREFIXES_BY_NAMESPACE;

    static {
        // Set up the standard namespaces ...
        Map<String, String> namespaces = new HashMap<String, String>();
        namespaces.put(DEFAULT_NAMESPACE_PREFIX, DEFAULT_NAMESPACE_URI);
        namespaces.put(JcrLexicon.Namespace.PREFIX, JcrLexicon.Namespace.URI);
        namespaces.put(JcrNtLexicon.Namespace.PREFIX, JcrNtLexicon.Namespace.URI);
        namespaces.put(JcrMixLexicon.Namespace.PREFIX, JcrMixLexicon.Namespace.URI);
        namespaces.put(JcrSvLexicon.Namespace.PREFIX, JcrSvLexicon.Namespace.URI);
        namespaces.put(XML_NAMESPACE_PREFIX, XML_NAMESPACE_URI);
        namespaces.put(XMLNS_NAMESPACE_PREFIX, XMLNS_NAMESPACE_URI);
        namespaces.put(XML_SCHEMA_NAMESPACE_PREFIX, XML_SCHEMA_NAMESPACE_URI);
        namespaces.put(XML_SCHEMA_INSTANCE_NAMESPACE_PREFIX, XML_SCHEMA_INSTANCE_NAMESPACE_URI);
        namespaces.put(ModeShapeLexicon.Namespace.PREFIX, ModeShapeLexicon.Namespace.URI);
        // Set up the reverse map for the standard namespaces ...
        Map<String, String> prefixes = new HashMap<String, String>();
        for (Map.Entry<String, String> entry : namespaces.entrySet()) {
            String uri = entry.getValue();
            String prefix = entry.getKey();
            prefixes.put(uri, prefix);
        }
        // Now set up the unmodifiable static collections ...
        STANDARD_BUILT_IN_NAMESPACES_BY_PREFIX = Collections.unmodifiableMap(namespaces);
        STANDARD_BUILT_IN_PREFIXES_BY_NAMESPACE = Collections.unmodifiableMap(prefixes);
        STANDARD_BUILT_IN_PREFIXES = Collections.unmodifiableSet(namespaces.keySet());
        STANDARD_BUILT_IN_URIS = Collections.unmodifiableSet(new HashSet<String>(namespaces.values()));
    }

    private final Behavior behavior;
    private final NamespaceRegistry registry;
    private final NamespaceRegistry workspaceRegistry;
    private final JcrSession session;

    protected JcrNamespaceRegistry( NamespaceRegistry workspaceRegistry,
                                    JcrSession session ) {
        this(Behavior.WORKSPACE, null, workspaceRegistry, session);
    }

    JcrNamespaceRegistry( Behavior behavior,
                          NamespaceRegistry localRegistry,
                          NamespaceRegistry workspaceRegistry,
                          JcrSession session ) {
        this.behavior = behavior;
        this.registry = localRegistry != null ? localRegistry : workspaceRegistry;
        this.workspaceRegistry = workspaceRegistry;
        this.session = session;

        assert this.behavior != null;
        assert this.registry != null;
        assert this.workspaceRegistry != null;
        assert this.session != null;
    }

    /**
     * Check that the session is still valid and {@link Session#isLive() live}.
     * 
     * @throws RepositoryException if the session is not valid or live
     */
    protected final void checkSession() throws RepositoryException {
        session.checkLive();
    }

    @Override
    public String getPrefix( String uri ) throws NamespaceException, RepositoryException {
        checkSession();
        if (behavior == Behavior.WORKSPACE) {
            // Check the standard ones first, ensuring that invalid changes to the persistent storage don't matter ...
            String prefix = STANDARD_BUILT_IN_PREFIXES_BY_NAMESPACE.get(uri);
            if (prefix != null) return prefix;
        }
        String prefix = registry.getPrefixForNamespaceUri(uri, false);
        if (prefix == null) {
            throw new NamespaceException(JcrI18n.noNamespaceWithUri.text(uri));
        }
        return prefix;
    }

    @Override
    public String[] getPrefixes() throws RepositoryException {
        checkSession();
        Set<Namespace> namespaces = registry.getNamespaces();
        String[] prefixes = new String[namespaces.size()];
        int i = 0;
        for (Namespace namespace : namespaces) {
            prefixes[i++] = namespace.getPrefix();
        }
        return prefixes;
    }

    @Override
    public String getURI( String prefix ) throws NamespaceException, RepositoryException {
        checkSession();
        if (behavior == Behavior.WORKSPACE) {
            // Check the standard ones first, ensuring that invalid changes to the persistent storage don't matter ...
            String uri = STANDARD_BUILT_IN_NAMESPACES_BY_PREFIX.get(prefix);
            if (uri != null) return uri;
        }
        String uri = registry.getNamespaceForPrefix(prefix);
        if (uri == null) {
            throw new NamespaceException(JcrI18n.noNamespaceWithPrefix.text(prefix));
        }
        return uri;
    }

    @Override
    public String[] getURIs() throws RepositoryException {
        checkSession();
        Set<Namespace> namespaces = registry.getNamespaces();
        String[] uris = new String[namespaces.size()];
        int i = 0;
        for (Namespace namespace : namespaces) {
            uris[i++] = namespace.getNamespaceUri();
        }
        return uris;
    }

    @Override
    public boolean isRegisteredPrefix( String prefix ) {
        return registry.getNamespaceForPrefix(prefix) != null;
    }

    @Override
    public boolean isRegisteredUri( String uri ) {
        return registry.isRegisteredNamespaceUri(uri);
    }

    @Override
    public synchronized void registerNamespace( String prefix,
                                                String uri ) throws NamespaceException, RepositoryException {
        CheckArg.isNotNull(prefix, "prefix");
        CheckArg.isNotNull(uri, "uri");
        checkSession();

        // Check the zero-length prefix and zero-length URI ...
        if (DEFAULT_NAMESPACE_PREFIX.equals(prefix) || DEFAULT_NAMESPACE_URI.equals(uri)) {
            throw new NamespaceException(JcrI18n.unableToChangeTheDefaultNamespace.text());
        }

        // Check whether the prefix begins with 'xml' (in any case) ...
        if (prefix.toLowerCase().startsWith(XML_NAMESPACE_PREFIX)) {
            throw new NamespaceException(JcrI18n.unableToRegisterNamespaceUsingXmlPrefix.text(prefix, uri));
        }

        // The prefix must be a valid XML Namespace prefix (i.e., a valid NCName) ...
        if (!XmlCharacters.isValidNcName(prefix)) {
            throw new NamespaceException(JcrI18n.unableToRegisterNamespaceWithInvalidPrefix.text(prefix, uri));
        }

        // The prefix must also be a valid JCR name ...
        try {
            session.nameFactory().create(prefix);
        } catch (ValueFormatException e) {
            throw new NamespaceException(JcrI18n.unableToRegisterNamespaceWithInvalidPrefix.text(prefix, uri));
        }

        String existingPrefixForURI = registry.getPrefixForNamespaceUri(uri, false);
        if (prefix.equals(existingPrefixForURI)) {
            //this is a no-op because we're trying to register the URI under the same prefix
            return;
        }

        boolean global = false;
        switch (behavior) {
            case SESSION:
                // --------------------------------------
                // JSR-283 Session remapping behavior ...
                // --------------------------------------
                // Section 5.11:
                // "All local mappings already present in the Session that include either the specified prefix
                // or the specified uri are removed and the new mapping is added."
                String existingUriForPrefix = registry.getNamespaceForPrefix(prefix);
                if (existingUriForPrefix != null) {
                    registry.unregister(existingUriForPrefix);
                }
                registry.unregister(uri);

                break;
            case WORKSPACE:
                // --------------------------------------------------
                // JSR-170 & JSR-283 Workspace namespace registry ...
                // --------------------------------------------------
                global = true;
                // Check whether the prefix or URI are reserved (case-sensitive) ...
                if (STANDARD_BUILT_IN_PREFIXES.contains(prefix)) {
                    throw new NamespaceException(JcrI18n.unableToRegisterReservedNamespacePrefix.text(prefix, uri));
                }
                if (STANDARD_BUILT_IN_URIS.contains(uri)) {
                    throw new NamespaceException(JcrI18n.unableToRegisterReservedNamespaceUri.text(prefix, uri));
                }

                try {
                    session.checkWorkspacePermission(session.workspaceName(), ModeShapePermissions.REGISTER_NAMESPACE);
                } catch (AccessControlException ace) {
                    throw new AccessDeniedException(ace);
                }

                if (existingPrefixForURI != null) {
                    //the uri is already mapped under a different prefix so check that it's not in use
                    checkURINotInUse(uri);
                }
                registry.unregister(uri);
                break;
            default:
                assert false; // should never happen
        }

        // Now we're sure the prefix and URI are valid and okay for a custom mapping ...
        try {
            registry.register(prefix, uri);
            // Signal the local node type manager ...
            session.signalNamespaceChanges(global);
        } catch (RuntimeException e) {
            throw new RepositoryException(e.getMessage(), e.getCause());
        }
    }

    @Override
    public synchronized void unregisterNamespace( String prefix )
        throws NamespaceException, AccessDeniedException, RepositoryException {
        CheckArg.isNotNull(prefix, "prefix");
        checkSession();

        // Don't need to check permissions for transient registration/unregistration
        if (behavior.equals(Behavior.WORKSPACE)) {
            try {
                session.checkWorkspacePermission(session.workspaceName(), ModeShapePermissions.REGISTER_NAMESPACE);
            } catch (AccessControlException ace) {
                throw new AccessDeniedException(ace);
            }
        }

        // Look to see whether the prefix is registered ...
        String uri = registry.getNamespaceForPrefix(prefix);
        // It is an error to unregister a namespace that is not registered ...
        if (uri == null) {
            throw new NamespaceException(JcrI18n.unableToUnregisterPrefixForNamespaceThatIsNotRegistered.text(prefix));
        }
        // Unregistering a built-in prefix or URI is invalid ...
        if (STANDARD_BUILT_IN_PREFIXES.contains(prefix)) {
            throw new NamespaceException(JcrI18n.unableToUnregisterReservedNamespacePrefix.text(prefix, uri));
        }
        if (STANDARD_BUILT_IN_URIS.contains(uri)) {
            throw new NamespaceException(JcrI18n.unableToUnregisterReservedNamespaceUri.text(prefix, uri));
        }

        // Do not allow to unregister a namespace which is used by a node type
        checkURINotInUse(uri);

        boolean global = false;
        switch (behavior) {
            case WORKSPACE: {
                global = true;
                break;
            }
            case SESSION: {
                break;
            }
            default: {
                //should never happen
                assert false;
            }
        }

        // Now we're sure the prefix is valid and is actually used in a mapping ...
        try {
            registry.unregister(uri);
            session.signalNamespaceChanges(global);
        } catch (RuntimeException e) {
            throw new RepositoryException(e.getMessage(), e.getCause());
        }
    }

    private void checkURINotInUse( String uri ) throws RepositoryException {
        NodeTypes nodeTypes = session.nodeTypes();
        for (Name nodeTypeName : nodeTypes.getAllNodeTypeNames()) {
            if (nodeTypeName.getNamespaceUri().equals(uri)) {
                throw new NamespaceException(JcrI18n.unableToUnregisterPrefixForNamespaceUsedByNodeType.text(uri, nodeTypeName));
            }
        }
    }

    @Override
    public String registerNamespace( String uri )
        throws UnsupportedRepositoryOperationException, AccessDeniedException, RepositoryException {
        CheckArg.isNotNull(uri, "uri");
        checkSession();

        // See if the namespace is already registered, so this will handle all built-in namespaces ...
        String prefix = registry.getPrefixForNamespaceUri(uri, false);
        if (prefix != null) {
            return prefix; // permission checks necessary ...
        }

        boolean global = false;
        switch (behavior) {
            case SESSION:
                // --------------------------------------
                // JSR-283 Session remapping behavior ...
                // --------------------------------------
                break;
            case WORKSPACE:
                // --------------------------------------------------
                // JSR-170 & JSR-283 Workspace namespace registry ...
                // --------------------------------------------------
                global = true;
                try {
                    session.checkWorkspacePermission(session.workspaceName(), ModeShapePermissions.REGISTER_NAMESPACE);
                } catch (AccessControlException ace) {
                    throw new AccessDeniedException(ace);
                }
                break;
            default:
                assert false; // should never happen
        }

        // Now we're sure the prefix and URI are valid and okay for a custom mapping ...
        try {
            prefix = registry.getPrefixForNamespaceUri(uri, true);
            // Signal the local node type manager ...
            session.signalNamespaceChanges(global);
            return prefix;
        } catch (RuntimeException e) {
            throw new RepositoryException(e.getMessage(), e.getCause());
        }
    }

    @Override
    public String toString() {
        return registry.toString();
    }
}
