/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.admin.registry;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.search.QueryParameterDefinition;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.namespace.NamespaceException;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.util.EqualsHelper;
import org.alfresco.util.Pair;
import org.alfresco.util.PropertyCheck;
import org.alfresco.util.PropertyMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Implementation of registry service to provide generic storage
 * and retrieval of system-related metadata.
 * 
 * @author Derek Hulley
 */
public class RegistryServiceImpl implements RegistryService
{
    private static Log logger = LogFactory.getLog(RegistryServiceImpl.class);
    
    private NamespaceService namespaceService;
    private NodeService nodeService;
    private SearchService searchService;
    private StoreRef registryStoreRef;
    private String registryRootPath;
    
    public void setNamespaceService(NamespaceService namespaceService)
    {
        this.namespaceService = namespaceService;
    }

    public void setNodeService(NodeService nodeService)
    {
        this.nodeService = nodeService;
    }

    public void setSearchService(SearchService searchService)
    {
        this.searchService = searchService;
    }

    /**
     * @param registryStoreRef the store in which the registry root is found
     */
    public void setRegistryStoreRef(StoreRef registryStoreRef)
    {
        this.registryStoreRef = registryStoreRef;
    }
    
    /**
     * @see #setRegistryStoreRef(StoreRef)
     */
    public void setRegistryStore(String registryStore)
    {
        this.setRegistryStoreRef(new StoreRef(registryStore));
    }

    /**
     * A root path e.g. <b>/sys:systemRegistry</b>
     * 
     * @param registryRootPath the path to the root of the registry
     */
    public void setRegistryRootPath(String registryRootPath)
    {
        this.registryRootPath = registryRootPath;
    }

    public void init()
    {
        // Check the properties
        PropertyCheck.mandatory(this, "namespaceService", namespaceService);
        PropertyCheck.mandatory(this, "nodeService", nodeService);
        PropertyCheck.mandatory(this, "registryRootPath", searchService);
        PropertyCheck.mandatory(this, "registryStore", registryStoreRef);
        PropertyCheck.mandatory(this, "registryRootPath", registryRootPath);
    }
    
    private NodeRef getRegistryRootNodeRef()
    {
        NodeRef registryRootNodeRef = null;
        // Ensure that the registry root node is present
        NodeRef storeRootNodeRef = nodeService.getRootNode(registryStoreRef);
        List<NodeRef> nodeRefs = searchService.selectNodes(
                storeRootNodeRef,
                registryRootPath,
                new QueryParameterDefinition[] {},
                namespaceService,
                false,
                SearchService.LANGUAGE_XPATH);
        if (nodeRefs.size() == 0)
        {
            throw new AlfrescoRuntimeException(
                    "Registry root not present: \n" +
                    "   Store: " + registryStoreRef + "\n" +
                    "   Path:  " + registryRootPath);
        }
        else if (nodeRefs.size() > 1)
        {
            throw new AlfrescoRuntimeException(
                    "Registry root path has multiple targets: \n" +
                    "   Store: " + registryStoreRef + "\n" +
                    "   Path:  " + registryRootPath);
        }
        else
        {
            registryRootNodeRef = nodeRefs.get(0);
        }
        
        // Check the root
        QName typeQName = nodeService.getType(registryRootNodeRef);
        if (!typeQName.equals(ContentModel.TYPE_CONTAINER))
        {
            throw new AlfrescoRuntimeException(
                    "Registry root is not of type " + ContentModel.TYPE_CONTAINER + ": \n" +
                    "   Node: " + registryRootNodeRef + "\n" +
                    "   Type: " + typeQName);
        }
        // Done
        if (logger.isDebugEnabled())
        {
            logger.debug(
                    "Found root for registry: \n" +
                    "   Store: " + registryStoreRef + "\n" +
                    "   Path : " + registryRootPath + "\n" +
                    "   Root:  " + registryRootNodeRef);
        }
        return registryRootNodeRef;
    }
    
    /**
     * Get the node-qname pair for the key.  If the key doesn't have a value element,
     * i.e. if it is purely path-based, then the QName will be null.
     * 
     * @return Returns the node and property name represented by the key or <tt>null</tt>
     *      if it doesn't exist and was not allowed to be created.
     */
    private Pair<NodeRef, QName> getPath(RegistryKey key, boolean create)
    {
        // Get the root
        NodeRef currentNodeRef = getRegistryRootNodeRef();
        // Get the key and property
        String namespaceUri = key.getNamespaceUri();
        String[] pathElements = key.getPath();
        String property = key.getProperty();
        // Find the node and property to put the value
        for (String pathElement : pathElements)
        {
            QName assocQName = QName.createQName(
                    namespaceUri,
                    QName.createValidLocalName(pathElement));
            
            // Find the node
            List<ChildAssociationRef> childAssocRefs = nodeService.getChildAssocs(
                    currentNodeRef,
                    ContentModel.ASSOC_CHILDREN,
                    assocQName);
            int size = childAssocRefs.size();
            if (size == 0)                          // Found nothing with that path
            {
                if (create)                         // Must create the path
                {
                    // Create the node (with a name)
                    PropertyMap properties = new PropertyMap();
                    properties.put(ContentModel.PROP_NAME, pathElement);
                    currentNodeRef = nodeService.createNode(
                            currentNodeRef,
                            ContentModel.ASSOC_CHILDREN,
                            assocQName,
                            ContentModel.TYPE_CONTAINER,
                            properties).getChildRef();
                }
                else
                {
                    // There is no node and we are not allowed to create it
                    currentNodeRef = null;
                    break;
                }
            }
            else                                    // Found some results for that path
            {
                if (size > 1 && create)             // More than one association by that name
                {
                    // Too many, so trim it down
                    boolean first = true;
                    for (ChildAssociationRef assocRef : childAssocRefs)
                    {
                        if (first)
                        {
                            first = false;
                            continue;
                        }
                        // Remove excess assocs
                        nodeService.removeChildAssociation(assocRef);
                    }
                }
                // Use the first one
                currentNodeRef = childAssocRefs.get(0).getChildRef();
            }
        }
        // Cater for null properties, i.e. path-based keys
        QName propertyQName = null;
        if (property != null)
        {
            propertyQName = QName.createQName(
                    namespaceUri,
                    QName.createValidLocalName(property));
        }
        // Create the result
        Pair<NodeRef, QName> resultPair = new Pair<NodeRef, QName>(currentNodeRef, propertyQName);
        // done
        if (logger.isDebugEnabled())
        {
            logger.debug("Converted registry key: \n" +
                    "   Key:      " + key + "\n" +
                    "   Result:   " + resultPair);
        }
        if (resultPair.getFirst() == null)
        {
            return null;
        }
        else
        {
            return resultPair;
        }
    }

    /**
     * {@inheritDoc}
     */
    public void addProperty(RegistryKey key, Serializable value)
    {
        if (key.getProperty() == null)
        {
            throw new IllegalArgumentException("Registry values must be added using paths that contain property names: " + key);
        }
        // Check the namespace being used in the key
        String namespaceUri = key.getNamespaceUri();
        if (!namespaceService.getURIs().contains(namespaceUri))
        {
            throw new NamespaceException("Unable to add a registry value with an unregistered namespace: " + namespaceUri);
        }
        
        // Get the path, with creation support
        Pair<NodeRef, QName> keyPair = getPath(key, true);
        // We know that the node exists, so just set the value
        nodeService.setProperty(keyPair.getFirst(), keyPair.getSecond(), value);
        // Done
        if (logger.isDebugEnabled())
        {
            logger.debug("Added value to registry: \n" +
                    "   Key:   " + key + "\n" +
                    "   Value: " + value);
        }
    }

    public Serializable getProperty(RegistryKey key)
    {
        if (key.getProperty() == null)
        {
            throw new IllegalArgumentException("Registry values must be fetched using paths that contain property names: " + key);
        }
        // Get the path, without creating
        Pair<NodeRef, QName> keyPair = getPath(key, false);
        Serializable property = null;
        if (keyPair != null)
        {
            property = nodeService.getProperty(keyPair.getFirst(), keyPair.getSecond());
        }
        // Done
        if (logger.isDebugEnabled())
        {
            logger.debug("Retrieved property from registry: \n" +
                    "   Key:   " + key + "\n" +
                    "   Value: " + property);
        }
        return property;
    }

    public Collection<String> getChildElements(RegistryKey key)
    {
        // Get the path without creating it
        Pair<NodeRef, QName> keyPair = getPath(key, false);
        if (keyPair == null)
        {
            // Nothing at that path
            return Collections.<String>emptyList();
        }
        // Use a query to find the children
        RegexQNamePattern qnamePattern = new RegexQNamePattern(key.getNamespaceUri(), ".*");
        List<ChildAssociationRef> childAssocRefs = nodeService.getChildAssocs(
                keyPair.getFirst(),
                ContentModel.ASSOC_CHILDREN,
                qnamePattern);
        // The localname of each one of the child associations represents a path element
        Collection<String> results = new ArrayList<String>(childAssocRefs.size());
        for (ChildAssociationRef assocRef : childAssocRefs)
        {
            results.add(assocRef.getQName().getLocalName());
        }
        // Done
        if (logger.isDebugEnabled())
        {
            logger.debug("Retrieved child elements from registry: \n" +
                    "   Key:      " + key + "\n" +
                    "   Elements: " + results);
        }
        return results;
    }

    public void copy(RegistryKey sourceKey, RegistryKey targetKey)
    {
        if ((sourceKey.getProperty() == null) && !(targetKey.getProperty() == null))
        {
            throw new AlfrescoRuntimeException(
                    "Registry keys must both be path specific for a copy: \n" +
                    "   Source: " + sourceKey + "\n" +
                    "   Target: " + targetKey);
        }
        else if ((sourceKey.getProperty() != null) && (targetKey.getProperty() == null))
        {
            throw new AlfrescoRuntimeException(
                    "Registry keys must both be value specific for a copy: \n" +
                    "   Source: " + sourceKey + "\n" +
                    "   Target: " + targetKey);
        }
        // If the source is missing, then do nothing
        Pair<NodeRef, QName> sourceKeyPair = getPath(sourceKey, false);
        if (sourceKeyPair == null)
        {
            if (logger.isDebugEnabled())
            {
                logger.debug("Nothing copied from non-existent registry source key: \n" +
                        "   Source: " + sourceKey + "\n" +
                        "   Target: " + targetKey);
            }
            return;
        }
        // Move based on the path or property
        Pair<NodeRef, QName> targetKeyPair = getPath(targetKey, true);
        if (sourceKeyPair.getSecond() != null)
        {
            // It is property-based so we just need to copy the value
            Serializable value = nodeService.getProperty(sourceKeyPair.getFirst(), sourceKeyPair.getSecond());
            nodeService.setProperty(targetKeyPair.getFirst(), targetKeyPair.getSecond(), value);
        }
        else
        {
            // It is path based so we need to copy all registry entries
            // We have an existing target, but we need to recurse
            Set<NodeRef> processedNodeRefs = new HashSet<NodeRef>(20);
            copyRecursive(sourceKey, targetKey, processedNodeRefs);
        }
        // Done
        if (logger.isDebugEnabled())
        {
            logger.debug("Copied registry keys: \n" +
                    "   Source: " + sourceKey + "\n" +
                    "   Target: " + targetKey);
        }
    }
    
    /**
     * @param sourceKey             the source path that must exist
     * @param targetKey             the target path that will be created
     * @param processedNodeRefs     a set to help avoid infinite loops
     */
    private void copyRecursive(RegistryKey sourceKey, RegistryKey targetKey, Set<NodeRef> processedNodeRefs)
    {
        String sourceNamespaceUri = sourceKey.getNamespaceUri();
        String targetNamespaceUri = targetKey.getNamespaceUri();
        // The source just exist
        Pair<NodeRef, QName> sourceKeyPair = getPath(sourceKey, false);
        if (sourceKeyPair == null)
        {
            // It has disappeared
            return;
        }
        NodeRef sourceNodeRef = sourceKeyPair.getFirst();
        // Check that we don't have a circular reference
        if (processedNodeRefs.contains(sourceNodeRef))
        {
            // This is very serious, but it can be worked around
            logger.error("Circular paths detected in registry entries: \n" +
                    "   Current Source Key: " + sourceKey + "\n" +
                    "   Current Target Key: " + targetKey + "\n" +
                    "   Source Node:        " + sourceNodeRef);
            logger.error("Bypassing circular registry entry");
            return;
        }

        // Make sure that the target exists
        Pair<NodeRef, QName> targetKeyPair = getPath(targetKey, true);
        NodeRef targetNodeRef = targetKeyPair.getFirst();
        
        // Copy properties of the source namespace
        Map<QName, Serializable> sourceProperties = nodeService.getProperties(sourceNodeRef);
        Map<QName, Serializable> targetProperties = nodeService.getProperties(targetNodeRef);
        boolean changed = false;
        for (Map.Entry<QName, Serializable> entry : sourceProperties.entrySet())
        {
            QName sourcePropertyQName = entry.getKey();
            if (!EqualsHelper.nullSafeEquals(sourcePropertyQName.getNamespaceURI(), sourceNamespaceUri))
            {
                // Wrong namespace
                continue;
            }
            // Copy the value over
            Serializable value = entry.getValue();
            QName targetPropertyQName = QName.createQName(targetNamespaceUri, sourcePropertyQName.getLocalName());
            targetProperties.put(targetPropertyQName, value);
            changed = true;
        }
        if (changed)
        {
            nodeService.setProperties(targetNodeRef, targetProperties);
        }
        // We have processed the source node
        processedNodeRefs.add(sourceNodeRef);
        
        // Now get the child elements of the source
        Collection<String> sourceChildElements = getChildElements(sourceKey);
        String[] sourcePath = sourceKey.getPath();
        String[] childSourcePath = new String[sourcePath.length + 1];   //
        System.arraycopy(sourcePath, 0, childSourcePath, 0, sourcePath.length);
        String[] targetPath = targetKey.getPath();
        String[] childTargetPath = new String[targetPath.length + 1];   //
        System.arraycopy(targetPath, 0, childTargetPath, 0, targetPath.length);
        for (String sourceChildElement : sourceChildElements)
        {
            // Make the source child key using the current source namespace
            childSourcePath[sourcePath.length] = sourceChildElement;
            RegistryKey sourceChildKey = new RegistryKey(sourceNamespaceUri, childSourcePath, null);
            // Make the target child key using the current target namespace
            childTargetPath[targetPath.length] = sourceChildElement;
            RegistryKey targetChildKey = new RegistryKey(targetNamespaceUri, childTargetPath, null);
            // Recurse
            copyRecursive(sourceChildKey, targetChildKey, processedNodeRefs);
        }
    }

    public void delete(RegistryKey key)
    {
        Pair<NodeRef, QName> keyPair = getPath(key, false);
        if (keyPair == null)
        {
            if (logger.isDebugEnabled())
            {
                logger.debug("Nothing to delete for registry key: \n" +
                        "   Key: " + key);
            }
            return;
        }
        NodeRef pathNodeRef = keyPair.getFirst();
        QName propertyQName = keyPair.getSecond();
        if (propertyQName == null)
        {
            // This is a path-based deletion
            nodeService.deleteNode(pathNodeRef);
            if (logger.isDebugEnabled())
            {
                logger.debug("Performed path-based delete: \n" +
                        "   Key:  " + key + "\n" +
                        "   Node: " + pathNodeRef);
            }
        }
        else
        {
            // This is a value-based deletion
            nodeService.removeProperty(pathNodeRef, propertyQName);
            if (logger.isDebugEnabled())
            {
                logger.debug("Performed value-based delete: \n" +
                        "   Key:      " + key + "\n" +
                        "   Node:     " + pathNodeRef + "\n" +
                        "   Property: " + propertyQName);
            }
        }
        // Done
    }
}
