/*
 * Copyright (C) 2005-2010 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * 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 .
 */
package org.alfresco.repo.model.ml;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.node.MLPropertyInterceptor;
import org.alfresco.repo.policy.BehaviourFilter;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.TransactionListenerAdapter;
import org.alfresco.repo.transaction.TransactionalResourceHelper;
import org.alfresco.service.cmr.ml.ContentFilterLanguagesService;
import org.alfresco.service.cmr.ml.MultilingualContentService;
import org.alfresco.service.cmr.model.FileExistsException;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.model.FileNotFoundException;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
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.security.PermissionService;
import org.alfresco.service.cmr.version.VersionService;
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.PropertyMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.extensions.surf.util.I18NUtil;
/**
 * Multilingual support implementation.
 * 
 * The basic structure supported is that of a hidden container of type
 * cm:mlContainer containing one or more secondary children of
 * type cm:mlDocument.  One of these will have a matching locale
 * and is referred to as the pivot translation.  It is also possible
 * to have several transient cm:emptyTranslation instances that
 * live and die with the container until they get their own content.
 * 
 * It is not possible to guarantee that there is always a pivot translation
 * available in the set of sibling translations.  The strategy is to hide
 * all translations when there isn't a pivot translation available.  A
 * background task should be cleaning up the empty or invalid cm:mlContainer
 * instances.
 *
 * @author Derek Hulley
 * @author Philippe Dubois
 * @author Yannick Pignot
 */
public class MultilingualContentServiceImpl implements MultilingualContentService
{
    private static final QName QNAME_ASSOC_ML_ROOT = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "multilingualRoot");
    private static final String KEY_ML_CONTAINERS_TO_DELETE = "MultilingualContentServiceImpl.mlContainersToDelete";
    
    private static Log logger = LogFactory.getLog(MultilingualContentServiceImpl.class);
    private NodeService nodeService;
    private PermissionService permissionService;
    private ContentFilterLanguagesService contentFilterLanguagesService;
    private FileFolderService fileFolderService;
    private VersionService versionService;
    private BehaviourFilter policyBehaviourFilter;
    private final MLContainerCleaner mlContainerCleaner;
    public MultilingualContentServiceImpl()
    {
        mlContainerCleaner = new MLContainerCleaner();
    }
    /**
     * @return Returns a reference to the node that will hold all the cm:mlContainer nodes.
     */
    private NodeRef getMLContainerRoot()
    {
        NodeRef rootNodeRef = nodeService.getRootNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE);
        List assocRefs = nodeService.getChildAssocs(
                rootNodeRef,
                ContentModel.ASSOC_CHILDREN,
                QNAME_ASSOC_ML_ROOT);
        if (assocRefs.size() != 1)
        {
            throw new AlfrescoRuntimeException(
                    "Unable to find bootstrap location for ML Root using query: " + QNAME_ASSOC_ML_ROOT);
        }
        NodeRef mlRootNodeRef = assocRefs.get(0).getChildRef();
        // Done
        return mlRootNodeRef;
    }
    private static final QName QNAME_ML_CONTAINER = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "mlContainer");
    private static final QName QNAME_ML_TRANSLATION = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "mlTranslation");
    /**
     * @return Returns a new cm:mlContainer
     */
    private NodeRef makeMLContainer()
    {
        NodeRef mlContainerRootNodeRef = getMLContainerRoot();
        // Create the container
        PropertyMap versionProperties = new PropertyMap();
        //versionProperties.put(ContentModel.PROP_AUTO_VERSION, Boolean.FALSE);
        //versionProperties.put(ContentModel.PROP_INITIAL_VERSION, Boolean.FALSE);
        ChildAssociationRef assocRef = nodeService.createNode(
                mlContainerRootNodeRef,
                ContentModel.ASSOC_CHILDREN,
                QNAME_ML_CONTAINER,
                ContentModel.TYPE_MULTILINGUAL_CONTAINER,
                versionProperties);
        NodeRef mlContainerNodeRef = assocRef.getChildRef();
        // TODO: Examine the usage of versioning - why is autoversioning on and used in the UI?
        // The model makes the container versionable by default, but why?
        nodeService.addAspect(mlContainerNodeRef, ContentModel.ASPECT_VERSIONABLE, versionProperties);
        // Set the permissions to allow anything by anyone
        permissionService.setPermission(
                mlContainerNodeRef,
                PermissionService.ALL_AUTHORITIES,
                PermissionService.ALL_PERMISSIONS, true);
        permissionService.setPermission(
                mlContainerNodeRef,
                AuthenticationUtil.getGuestUserName(),
                PermissionService.ALL_PERMISSIONS, true);
        // Done
        return mlContainerNodeRef;
    }
    /**
     * Get the ML Container of the given node, allowing null
     * @param mlDocumentNodeRef     the translation
     * @param allowNull             true if a null value may be returned
     * @return                      Returns the cm:mlContainer or null if there isn't one
     * @throws AlfrescoRuntimeException if there is no container
     */
    private NodeRef getMLContainer(NodeRef mlDocumentNodeRef, boolean allowNull)
    {
        NodeRef mlContainerNodeRef = null;
        List parentAssocRefs = nodeService.getParentAssocs(
                mlDocumentNodeRef,
                ContentModel.ASSOC_MULTILINGUAL_CHILD,
                RegexQNamePattern.MATCH_ALL);
        if (parentAssocRefs.size() == 0)
        {
            if (!allowNull)
            {
                throw new AlfrescoRuntimeException(
                        "No multilingual container exists for document node: " + mlDocumentNodeRef);
            }
            mlContainerNodeRef = null;
        }
        else if (parentAssocRefs.size() >= 1)
        {
            // Just get it
            ChildAssociationRef toKeepAssocRef = parentAssocRefs.get(0);
            mlContainerNodeRef = toKeepAssocRef.getParentRef();
        }
        // Done
        return mlContainerNodeRef;
    }
    /**
     * Retrieve or create a cm:mlDocument container for the given node, which must have the
     * cm:mlDocument already applied.
     *
     * @param mlDocumentNodeRef an existing cm:mlDocument
     * @param allowCreate true if a cm:mlContainer must be created if on doesn't exist,
     *      otherwise false if a parent cm:mlContainer is expected to exist.
     * @return Returns the cm:mlContainer parent
     */
    private NodeRef getOrCreateMLContainer(NodeRef mlDocumentNodeRef, boolean allowCreate)
    {
        if (!nodeService.hasAspect(mlDocumentNodeRef, ContentModel.ASPECT_MULTILINGUAL_DOCUMENT))
        {
            throw new IllegalArgumentException(
                    "Node must have aspect " + ContentModel.ASPECT_MULTILINGUAL_DOCUMENT + " applied");
        }
        // Now check if a parent mlContainer exists
        NodeRef mlContainerNodeRef = null;
        boolean createAssociation = false;
        List parentAssocRefs = nodeService.getParentAssocs(
                mlDocumentNodeRef,
                ContentModel.ASSOC_MULTILINGUAL_CHILD,
                RegexQNamePattern.MATCH_ALL);
        if (parentAssocRefs.size() == 0)
        {
            if (allowCreate)
            {
                // Create a ML container
                mlContainerNodeRef = makeMLContainer();
                createAssociation = true;
            }
            else
            {
                throw new AlfrescoRuntimeException("No multilingual container exists for document node: " + mlDocumentNodeRef);
            }
        }
        else if (parentAssocRefs.size() == 1)
        {
            // Just get it
            ChildAssociationRef toKeepAssocRef = parentAssocRefs.get(0);
            mlContainerNodeRef = toKeepAssocRef.getParentRef();
        }
        else if (parentAssocRefs.size() > 1)
        {
            // This is a problem - destroy all but the first
            logger.warn("Cleaning up multiple multilingual containers on node: " + mlDocumentNodeRef);
            ChildAssociationRef toKeepAssocRef = parentAssocRefs.get(0);
            mlContainerNodeRef = toKeepAssocRef.getParentRef();
            // Remove all the associations to the container
            boolean first = true;
            for (ChildAssociationRef assocRef : parentAssocRefs)
            {
                if (first)
                {
                    first = false;
                    continue;
                }
                nodeService.removeChildAssociation(assocRef);
            }
        }
        // Associate the translation with the container
        if (createAssociation)
        {
            nodeService.addChild(
                    mlContainerNodeRef,
                    mlDocumentNodeRef,
                    ContentModel.ASSOC_MULTILINGUAL_CHILD,
                    QNAME_ML_TRANSLATION);
        }
        // done
        return mlContainerNodeRef;
    }
    private NodeRef makeTranslationImpl(NodeRef mlContainerNodeRef, NodeRef contentNodeRef, Locale locale)
    {
        // Previous versions of the document are not compatible with the versioning requirements
        // dictated by the aspects about to be added.  A version has to be forced if the aspect
        // already exists.
        // https://issues.alfresco.com/jira/browse/ETHREEOH-1657
        boolean forceNewVersion = nodeService.hasAspect(contentNodeRef, ContentModel.ASPECT_VERSIONABLE);
        // Add the aspect using the given locale, of necessary
        if (!nodeService.hasAspect(contentNodeRef, ContentModel.ASPECT_MULTILINGUAL_DOCUMENT))
        {
            PropertyMap properties = new PropertyMap();
            properties.put(ContentModel.PROP_LOCALE, locale);
            nodeService.addAspect(contentNodeRef, ContentModel.ASPECT_LOCALIZED, properties);
            nodeService.addAspect(contentNodeRef, ContentModel.ASPECT_MULTILINGUAL_DOCUMENT, null);
        }
        else
        {
            // The aspect is present, so just ensure that the locale is correct
            nodeService.setProperty(contentNodeRef, ContentModel.PROP_LOCALE, locale);
        }
        
        if (forceNewVersion)
        {
            versionService.createVersion(contentNodeRef, null);
        }
        // Do we make use of an existing container?
        if (mlContainerNodeRef == null)
        {
            // Make one
            mlContainerNodeRef = getOrCreateMLContainer(contentNodeRef, true);
            Serializable containerFunctionalName = nodeService.getProperty(contentNodeRef, ContentModel.PROP_NAME);
            // set the pivot language and the functional name
            nodeService.setProperty(mlContainerNodeRef, ContentModel.PROP_LOCALE, locale);
            nodeService.setProperty(mlContainerNodeRef, ContentModel.PROP_NAME, containerFunctionalName);
        }
        else
        {
            // ALF-2200: Create the translation as the same type as the pivot
            NodeRef pivotNodeRef = this.getPivotTranslation(mlContainerNodeRef);
            if (pivotNodeRef != null && !pivotNodeRef.equals(contentNodeRef))
            {
                QName pivotNodeType = nodeService.getType(pivotNodeRef);
                QName contentNodeType = nodeService.getType(contentNodeRef);
                if (!pivotNodeType.equals(contentNodeType))
                {
                    nodeService.setType(contentNodeRef, pivotNodeType);
                }
            }
            
            // Check that the language is not duplicated
            Map existingLanguages = this.getTranslations(mlContainerNodeRef);
            if (existingLanguages.containsKey(locale))
            {
                throw new AlfrescoRuntimeException("Duplicate locale in document pool: " + locale);
            }
            // Use the existing container
            nodeService.addChild(
                    mlContainerNodeRef,
                    contentNodeRef,
                    ContentModel.ASSOC_MULTILINGUAL_CHILD,
                    QNAME_ML_TRANSLATION);
        }
        
        // Make sure that we don't delete the container if it was previously scheduled for pre-commit deletion.
        // This arises when editions are going to be created
        TransactionalResourceHelper.getSet(KEY_ML_CONTAINERS_TO_DELETE).remove(mlContainerNodeRef);
        // done
        return mlContainerNodeRef;
    }
    private boolean isPivotTranslation(NodeRef contentNodeRef)
    {
        Locale locale = (Locale) nodeService.getProperty(contentNodeRef, ContentModel.PROP_LOCALE);
        // Get the container
        NodeRef containerNodeRef = getOrCreateMLContainer(contentNodeRef, false);
        Locale containerLocale = (Locale) nodeService.getProperty(containerNodeRef, ContentModel.PROP_LOCALE);
        boolean isPivot = EqualsHelper.nullSafeEquals(locale, containerLocale);
        // Done
        if (logger.isDebugEnabled())
        {
            logger.debug("Node " + (isPivot ? "is" : "is not") + " pivot: " + contentNodeRef);
        }
        return isPivot;
    }
    /** {@inheritDoc} */
    public boolean isTranslation(NodeRef contentNodeRef)
    {
        if (!nodeService.hasAspect(contentNodeRef, ContentModel.ASPECT_MULTILINGUAL_DOCUMENT))
        {
            // It doesn't have the aspect, so it isn't a translation
            if (logger.isDebugEnabled())
            {
                logger.debug("Document is not multilingual: " + contentNodeRef);
            }
            return false;
        }
        // Are there any associated translations
        Map translations = getTranslations(contentNodeRef);
        if (translations.size() > 0)
        {
            if (logger.isDebugEnabled())
            {
                logger.debug("Document is a translation: " + contentNodeRef);
            }
            return true;
        }
        else
        {
            if (logger.isDebugEnabled())
            {
                logger.debug("Document is not a translation: " + contentNodeRef);
            }
            return false;
        }
    }
    /** {@inheritDoc} */
    public void makeTranslation(NodeRef contentNodeRef, Locale locale)
    {
        NodeRef mlContainerNodeRef = makeTranslationImpl(null, contentNodeRef, locale);
        // done
        if (logger.isDebugEnabled())
        {
            logger.debug("Made a translation: \n" +
                    "   content:   " + contentNodeRef + "\n" +
                    "   locale:    " + locale + "\n" +
                    "   container: " + mlContainerNodeRef);
        }
    }
    /** @inheritDoc */
    public void deleteTranslationContainer(NodeRef mlContainerNodeRef)
    {
        if (!ContentModel.TYPE_MULTILINGUAL_CONTAINER.equals(nodeService.getType(mlContainerNodeRef)))
        {
            throw new IllegalArgumentException(
                    "Node type must be " + ContentModel.TYPE_MULTILINGUAL_CONTAINER);
        }
        // get the translations
        Map translations = this.getTranslations(mlContainerNodeRef);
        // remember the number of childs
        int translationCount = translations.size();
        // remove the translations
        for(NodeRef translationToRemove : translations.values())
        {
            if (!nodeService.exists(translationToRemove))
            {
                // We've just queried for these
                throw new ConcurrencyFailureException("Translation has been deleted externally: " + translationToRemove);
            }
            nodeService.deleteNode(translationToRemove);
        }
        // Keep track of the container for pre-commit deletion
        TransactionalResourceHelper.getSet(KEY_ML_CONTAINERS_TO_DELETE).add(mlContainerNodeRef);
        AlfrescoTransactionSupport.bindListener(mlContainerCleaner);
        // done
        if (logger.isDebugEnabled())
        {
            logger.debug("ML container removed: \n" +
                    "   Container:  " + mlContainerNodeRef + "\n" +
                    "   Number of translations: " + translationCount);
        }
    }
    
    /**
     * Does the work of making the translation a simple node again.  No parent-child relationships
     * are modified and the pivot-container logic is not done here.
     * 
     * @param translationNodeRef                a translation
     */
    private void unmakeTranslationSimple(NodeRef translationNodeRef)
    {      
        try
        {
            this.policyBehaviourFilter.disableBehaviour(ContentModel.TYPE_MULTILINGUAL_CONTAINER);
            if (nodeService.hasAspect(translationNodeRef, ContentModel.ASPECT_MULTILINGUAL_EMPTY_TRANSLATION))
            {
                nodeService.removeAspect(translationNodeRef, ContentModel.ASPECT_MULTILINGUAL_EMPTY_TRANSLATION);
                nodeService.addAspect(translationNodeRef, ContentModel.ASPECT_TEMPORARY, null);
            }
            else
            {
                if (nodeService.hasAspect(translationNodeRef, ContentModel.ASPECT_MULTILINGUAL_DOCUMENT))
                {
                    nodeService.removeAspect(translationNodeRef, ContentModel.ASPECT_MULTILINGUAL_DOCUMENT);
                }
            }
            List assocRefs = nodeService.getParentAssocs(
                    translationNodeRef,
                    ContentModel.ASSOC_MULTILINGUAL_CHILD, 
                    RegexQNamePattern.MATCH_ALL);
            if (assocRefs.size() != 1)
            {
                throw new AlfrescoRuntimeException(
                        "Unable to remove ASSOC_MULTILINGUAL_CHILD on : " + translationNodeRef.toString());
            }
        }
        finally
        {
            this.policyBehaviourFilter.enableBehaviour(ContentModel.TYPE_MULTILINGUAL_CONTAINER);
        }
    }
    /** @inheritDoc */
    public void unmakeTranslation(NodeRef translationNodeRef)
    {
        if ((nodeService.hasAspect(translationNodeRef, ContentModel.ASPECT_MULTILINGUAL_EMPTY_TRANSLATION) ||
                nodeService.hasAspect(translationNodeRef, ContentModel.ASPECT_MULTILINGUAL_DOCUMENT)) &&
                isPivotTranslation(translationNodeRef))
        {
            NodeRef containerNodeRef = getMLContainer(translationNodeRef, true);
            // We have not cleaned up all other translations
            // Mark the container for deletion
            TransactionalResourceHelper.getSet(KEY_ML_CONTAINERS_TO_DELETE).add(containerNodeRef);
            AlfrescoTransactionSupport.bindListener(mlContainerCleaner);
        }
        // Turns the document from a translation into a normal document by removing MLDocument aspect
        unmakeTranslationSimple(translationNodeRef);
    }
    /** {@inheritDoc} */
    public void addTranslation(NodeRef newTranslationNodeRef, NodeRef translationOfNodeRef, Locale locale)
    {
        // Get the container
        NodeRef mlContainerNodeRef = null;
        if(ContentModel.TYPE_MULTILINGUAL_CONTAINER.equals(nodeService.getType(translationOfNodeRef)))
        {
            mlContainerNodeRef = translationOfNodeRef;
        }
        else
        {
            mlContainerNodeRef = getOrCreateMLContainer(translationOfNodeRef, false);
        }
        // Use the existing container to make the new content into a translation
        makeTranslationImpl(mlContainerNodeRef, newTranslationNodeRef, locale);
        // done
        if (logger.isDebugEnabled())
        {
            logger.debug("Added a translation: \n" +
                    "   Translation of:  " + translationOfNodeRef + "\n" +
                    "   New translation: " + newTranslationNodeRef + "\n" +
                    "   Locale:          " + locale);
        }
    }
    /** {@inheritDoc} */
    public NodeRef getTranslationContainer(NodeRef translationNodeRef)
    {
        NodeRef mlContainerNodeRef = getOrCreateMLContainer(translationNodeRef, false);
        // done
        return mlContainerNodeRef;
    }
    /** {@inheritDoc} */
    public Map getTranslations(NodeRef translationOfNodeRef)
    {
        NodeRef mlContainerNodeRef = null;
        // Were we given the translation or the container
        QName typeQName = nodeService.getType(translationOfNodeRef);
        if (typeQName.equals(ContentModel.TYPE_MULTILINGUAL_CONTAINER))
        {
            // We have the container
            mlContainerNodeRef = translationOfNodeRef;
        }
        else
        {
            // Get the container
            mlContainerNodeRef = getOrCreateMLContainer(translationOfNodeRef, false);
        }
        // Get all the children
        List assocRefs = nodeService.getChildAssocs(
                mlContainerNodeRef,
                ContentModel.ASSOC_MULTILINGUAL_CHILD,
                RegexQNamePattern.MATCH_ALL);
        // Iterate over them and build the map
        Map nodeRefsByLocale = new HashMap(13);
        for (ChildAssociationRef assocRef : assocRefs)
        {
            NodeRef nodeRef = assocRef.getChildRef();
            // Get the locale
            Locale locale = (Locale) nodeService.getProperty(nodeRef, ContentModel.PROP_LOCALE);
            // Map it
            nodeRefsByLocale.put(locale, nodeRef);
        }
        // Done
        if (logger.isDebugEnabled())
        {
            logger.debug("Found all translations: \n" +
                    "   Node: " + translationOfNodeRef + " (type " + typeQName + ")\n" +
                    "   Map: " + nodeRefsByLocale);
        }
        return nodeRefsByLocale;
    }
    /** {@inheritDoc} */
    public NodeRef getTranslationForLocale(NodeRef translationNodeRef, Locale locale)
    {
        // Get all the translations
        Map nodeRefsByLocale = getTranslations(translationNodeRef);
        // Get the closest matching locale
        Set locales = nodeRefsByLocale.keySet();
        Locale nearestLocale = I18NUtil.getNearestLocale(locale, locales);
        NodeRef nearestNodeRef = nodeRefsByLocale.get(nearestLocale);
        if (nearestNodeRef == null)
        {
            // There is no translation for the locale, so get the pivot translation
            nearestNodeRef = getPivotTranslation(translationNodeRef);
            if (nearestNodeRef == null)
            {
                // There is no pivot translation, so just use the given node
                nearestNodeRef = translationNodeRef;
            }
        }
        // Done
        if (logger.isDebugEnabled())
        {
            logger.debug("Found nearest locale: \n" +
                    "   Given node:   " + translationNodeRef + "\n" +
                    "   Given locale: " + locale + "\n" +
                    "   Found node:   " + nearestNodeRef + "\n" +
                    "   Found locale: " + nearestLocale);
        }
        return nearestNodeRef;
    }
    /** {@inheritDoc} */
    public List getMissingTranslations(NodeRef localizedNodeRef, boolean addThisNodeLocale)
    {
        List foundLocales   = new ArrayList(getTranslations(localizedNodeRef).keySet());
        List foundLanguages = new ArrayList();
        // transform locales into languages codes
        for(Locale locale : foundLocales)
        {
            foundLanguages.add(locale.getLanguage());
        }
        //    add the locale of the given node if required
        if(addThisNodeLocale)
        {
            Locale localeNode = (Locale) nodeService.getProperty(localizedNodeRef, ContentModel.PROP_LOCALE);
            if(localeNode != null)
            {
                foundLanguages.remove(localeNode.toString());
            }
            else
            {
                logger.warn("No locale found for the node " + localizedNodeRef);
            }
        }
        List missingLanguages = null;
        if(foundLanguages.size() == 0)
        {
            // The given node is the only one available translation and it must
            // be return.
            // MissingLanguages become the entire list pf languages.
            missingLanguages = contentFilterLanguagesService.getFilterLanguages();
        }
        else
        {
            // get the missing languages form the list of content filter languages
            missingLanguages = contentFilterLanguagesService.getMissingLanguages(foundLanguages);
        }
        // construct a list of locales
        List missingLocales = new ArrayList(missingLanguages.size() + 1);
        for(String lang : missingLanguages)
        {
            missingLocales.add(I18NUtil.parseLocale(lang));
        }
        return missingLocales;
    }
    /** {@inheritDoc} */
    public NodeRef getPivotTranslation(NodeRef nodeRef)
    {
        Locale containerLocale = null;
        if(nodeService.hasAspect(nodeRef, ContentModel.ASPECT_MULTILINGUAL_DOCUMENT))
        {
            NodeRef container = getTranslationContainer(nodeRef);
            containerLocale = (Locale) nodeService.getProperty(container, ContentModel.PROP_LOCALE);
        }
        else if(ContentModel.TYPE_MULTILINGUAL_CONTAINER.equals(nodeService.getType(nodeRef)))
        {
            containerLocale = (Locale) nodeService.getProperty(nodeRef, ContentModel.PROP_LOCALE);
        }
        else
        {
            logger.warn("The node is not multilingual " + nodeRef);
        }
        // Get all the translations
        Map nodeRefsByLocale = getTranslations(nodeRef);
        // Get the closest matching locale
        Set locales = nodeRefsByLocale.keySet();
        Locale nearestLocale = I18NUtil.getNearestLocale(containerLocale, locales);
        if (nearestLocale == null)
        {
            // There is no pivot translation
            return null;
        }
        else
        {
            return nodeRefsByLocale.get(nearestLocale);
        }
    }
    /**
     * {@inheritDoc}
     */
    public NodeRef addEmptyTranslation(NodeRef translationOfNodeRef, String name, Locale locale)
    {
        boolean hasMLAspect = nodeService.hasAspect(translationOfNodeRef, ContentModel.ASPECT_MULTILINGUAL_DOCUMENT);
        boolean isMLContainer = nodeService.getType(translationOfNodeRef).equals(ContentModel.TYPE_MULTILINGUAL_CONTAINER);
        if (hasMLAspect || isMLContainer)
        {
            // Get the pivot translation
            NodeRef pivotTranslationNodeRef = getPivotTranslation(translationOfNodeRef);
            if (pivotTranslationNodeRef != null)
            {
                // We found a pivot translation, so use it
                translationOfNodeRef = pivotTranslationNodeRef;
            }
            else
            {
                // We use the given translation
            }
        }
        else
        {
            throw new IllegalArgumentException(
                    "Node must have aspect " + ContentModel.ASPECT_MULTILINGUAL_DOCUMENT + ": \n" +
                    "   Translation: " + translationOfNodeRef + "\n" +
                    "   Locale:      " + locale);
        }
        FileInfo translationOfFileInfo = fileFolderService.getFileInfo(translationOfNodeRef);
        String translationOfName = translationOfFileInfo.getName();
        // If name is null, supply one
        if (name == null)
        {
            name = translationOfName;
        }
        // If there is a name clash, add the locale to the main portion of the filename
        if (name.equalsIgnoreCase(translationOfName))
        {
            String localeStr = locale.toString();
            if (localeStr.endsWith("_"))
            {
                localeStr = localeStr.substring(0, localeStr.length() - 1);
            }
            String rawName;
            String extension;
            int index = name.lastIndexOf('.');
            if (index > 0)
            {
                rawName = name.substring(0, index);
                extension = "." + name.substring(index + 1);
            }
            else
            {
                rawName = name;
                extension = "";                 // No extension
            }
            name = rawName + "_" + localeStr + extension;
        }
        // Create the document in the space of the node of reference
        NodeRef parentNodeRef = nodeService.getPrimaryParent(translationOfNodeRef).getParentRef();
        // Create the empty translation.
        // ALF-2200: Create the translation as the same type as the pivot
        QName newTranslationType = nodeService.getType(translationOfNodeRef);
        NodeRef newTranslationNodeRef = fileFolderService.create(
                parentNodeRef,
                name,
                newTranslationType).getNodeRef();
        // add the translation to the container
        addTranslation(newTranslationNodeRef, translationOfNodeRef, locale);
        // Although the content is spoofed from the pivot translation, it isn't done for all services
        // TODO: Fix http://issues.alfresco.com/browse/AR-1487
        ContentData translationOfContentData = (ContentData) nodeService.getProperty(translationOfNodeRef, ContentModel.PROP_CONTENT);
        if (translationOfContentData != null)
        {
            ContentData newTranslationContentData = new ContentData(
                    null,
                    translationOfContentData.getMimetype(),
                    translationOfContentData.getSize(),
                    translationOfContentData.getEncoding(),
                    translationOfContentData.getLocale());
            nodeService.setProperty(newTranslationNodeRef, ContentModel.PROP_CONTENT, newTranslationContentData);
        }
        // set it empty
        nodeService.addAspect(newTranslationNodeRef, ContentModel.ASPECT_MULTILINGUAL_EMPTY_TRANSLATION, null);
        // Initially, the file should be temporary.  This will be changed as soon as some content is added.
        nodeService.addAspect(newTranslationNodeRef, ContentModel.ASPECT_TEMPORARY, null);
        if (logger.isDebugEnabled())
        {
            logger.debug("Added an empty translation: \n" +
                    "   Translation of:  " + translationOfNodeRef + "\n" +
                    "   New translation: " + newTranslationNodeRef + "\n" +
                    "   Locale:          " + locale);
        }
        return newTranslationNodeRef;
    }
    /**
     * @inheritDoc
     */
    public NodeRef copyTranslationContainer(NodeRef mlContainerNodeRef, NodeRef newParentRef, String prefixName) throws Exception
    {
        // There is no need for the properties interceptor here
        boolean wasMLAware = MLPropertyInterceptor.setMLAware(true);
        
        if(!ContentModel.TYPE_MULTILINGUAL_CONTAINER.equals(nodeService.getType(mlContainerNodeRef)))
        {
            throw new IllegalArgumentException(
                    "Node type must be " + ContentModel.TYPE_MULTILINGUAL_CONTAINER);
        }
        // if the container has no translation: nothing to do
        if(nodeService.getChildAssocs(mlContainerNodeRef, ContentModel.ASSOC_MULTILINGUAL_CHILD, RegexQNamePattern.MATCH_ALL).size() < 1)
        {
            if (logger.isDebugEnabled())
            {
                logger.debug("MLContainer has no translation "  + mlContainerNodeRef);
            }
            return null;
        }
        // keep a reference to the containing space before copy
        NodeRef spaceBefore = nodeService.getPrimaryParent(getPivotTranslation(mlContainerNodeRef)).getParentRef();
        if(spaceBefore.equals(newParentRef))
        {
            throw new AlfrescoRuntimeException(
                    "Impossible to copy the mlContainer, source folder is the same as the destination container.");
        }
        // get the pivot translation and its locale
        NodeRef pivotNodeRef = getPivotTranslation(mlContainerNodeRef);
        Locale pivotLocale = (Locale) nodeService.getProperty(pivotNodeRef, ContentModel.PROP_LOCALE);
        String pivotName = prefixName + (String) nodeService.getProperty(pivotNodeRef, ContentModel.PROP_NAME);
        if(prefixName == null)
        {
            prefixName = "";
        }
        NodeRef pivotCopyNodeRef = null;
        pivotCopyNodeRef = fileFolderService.copy(pivotNodeRef, newParentRef, pivotName).getNodeRef();
        // make the new pivot multilingual
        this.makeTranslation(pivotCopyNodeRef, pivotLocale);
        // get a reference to the new mlContainer
        NodeRef newMLContainerNodeRef = getMLContainer(pivotCopyNodeRef, false);
        // copy each other translation and make them multilingual too
        for(Map.Entry entry : getTranslations(mlContainerNodeRef).entrySet())
        {
            Locale translationLocale = entry.getKey();
            NodeRef translationNodeRef = entry.getValue();
            String name = prefixName + (String) nodeService.getProperty(translationNodeRef, ContentModel.PROP_NAME);
            if(!translationNodeRef.equals(pivotNodeRef))
            {
               if(nodeService.hasAspect(translationNodeRef, ContentModel.ASPECT_MULTILINGUAL_EMPTY_TRANSLATION))
                {
                    // Turn off any empty translation policy behaviours to enabled the copy.
                    this.policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_MULTILINGUAL_EMPTY_TRANSLATION);
                    this.policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_MULTILINGUAL_DOCUMENT);
                    try
                    {
                        // copy the translation
                        NodeRef copyNodeRef = fileFolderService.copy(translationNodeRef, newParentRef, name).getNodeRef();
                        // Add it to the newMLContainer
                        nodeService.addChild(
                                newMLContainerNodeRef,
                                copyNodeRef,
                                ContentModel.ASSOC_MULTILINGUAL_CHILD,
                                QNAME_ML_TRANSLATION);
                        
                        // Add the ML aspects back
                        nodeService.addAspect(translationNodeRef, ContentModel.ASPECT_MULTILINGUAL_DOCUMENT, null);
                        nodeService.addAspect(translationNodeRef, ContentModel.ASPECT_MULTILINGUAL_EMPTY_TRANSLATION, null);
                    }
                    finally
                    {
                        this.policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_MULTILINGUAL_EMPTY_TRANSLATION);
                        this.policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_MULTILINGUAL_DOCUMENT);
                    }
                }
                else
                {
                    // copy the translation
                    NodeRef copyNodeRef = fileFolderService.copy(translationNodeRef, newParentRef, name).getNodeRef();
                    // add it to the mlContainer
                    this.addTranslation(copyNodeRef, newMLContainerNodeRef, translationLocale);
                    // set its locale property
                    nodeService.setProperty(copyNodeRef, ContentModel.PROP_LOCALE, translationLocale);
                }
            }
            else
            {
                // the pivot is already created
            }
        }
        // The rest of the transaction can have properties modified
        MLPropertyInterceptor.setMLAware(wasMLAware);
        
        if (logger.isDebugEnabled())
        {
            logger.debug("MLContainer copied: \n" +
                    "   Copy of : " + mlContainerNodeRef + "(translations located in " + spaceBefore + ") \n" +
                    "   Copy :  " + newMLContainerNodeRef + "(translations located in " + newParentRef + ") \n");
        }
        return newMLContainerNodeRef;
    }
    /**
     * @inheritDoc
     */
    public void moveTranslationContainer(NodeRef mlContainerNodeRef, NodeRef newParentRef) throws FileExistsException, FileNotFoundException
    {
        if(!ContentModel.TYPE_MULTILINGUAL_CONTAINER.equals(nodeService.getType(mlContainerNodeRef)))
        {
            throw new IllegalArgumentException(
                    "Node type must be " + ContentModel.TYPE_MULTILINGUAL_CONTAINER);
        }
        // if the container has no translation: nothing to do
        if(nodeService.getChildAssocs(mlContainerNodeRef, ContentModel.ASSOC_MULTILINGUAL_CHILD, RegexQNamePattern.MATCH_ALL).size() < 1)
        {
            if (logger.isDebugEnabled())
            {
                logger.debug("MLContainer has no translation " + mlContainerNodeRef);
            }
            return;
        }
        // keep a reference to the containing space before moving
        NodeRef spaceBefore = nodeService.getPrimaryParent(getPivotTranslation(mlContainerNodeRef)).getParentRef();
        if(spaceBefore.equals(newParentRef))
        {
            // nothing to do
            return;
        }
        // move each translation
        for(NodeRef translationToMove : getTranslations(mlContainerNodeRef).values())
        {
            fileFolderService.move(translationToMove, newParentRef, null);
        }
        if (logger.isDebugEnabled())
        {
            logger.debug("MLContainer moved: \n" +
                    "   Old location of " + mlContainerNodeRef + " : " + spaceBefore + ") \n" +
                    "   New location of " + mlContainerNodeRef + " : " + newParentRef + ")");
        }
    }
    /**
     * Cleans up any ml:container types that are empty or have lost their pivot translation
     * 
     * @author Derek Hulley
     * @since 4.1.1
     */
    private class MLContainerCleaner extends TransactionListenerAdapter
    {
        @Override
        public void beforeCommit(boolean readOnly)
        {
            if (readOnly)
            {
                return;             // Don't ever expect to be here
            }
            Set mlContainerNodeRefs = TransactionalResourceHelper.getSet(KEY_ML_CONTAINERS_TO_DELETE);
            for (NodeRef mlContainerNodeRef : mlContainerNodeRefs)
            {
                // Just delete it.
                // Any remaining translations will be cleaned up
                nodeService.deleteNode(mlContainerNodeRef);
            }
        }
    }
    public void setNodeService(NodeService nodeService)
    {
        this.nodeService = nodeService;
    }
    public void setPermissionService(PermissionService permissionService)
    {
        this.permissionService = permissionService;
    }
    public void setContentFilterLanguagesService(ContentFilterLanguagesService contentFilterLanguagesService)
    {
        this.contentFilterLanguagesService = contentFilterLanguagesService;
    }
    public void setFileFolderService(FileFolderService fileFolderService)
    {
        this.fileFolderService = fileFolderService;
    }
    public void setVersionService(VersionService versionService)
    {
        this.versionService = versionService;
    }
    public void setPolicyBehaviourFilter(BehaviourFilter policyBehaviourFilter)
    {
        this.policyBehaviourFilter = policyBehaviourFilter;
    }
}