/*
 * #%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.dictionary;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.query.EmptyPagingResults;
import org.alfresco.query.PageDetails;
import org.alfresco.query.PagingRequest;
import org.alfresco.query.PagingResults;
import org.alfresco.repo.admin.RepoAdminServiceImpl;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.download.DownloadModel;
import org.alfresco.repo.download.DownloadStorage;
import org.alfresco.repo.model.filefolder.HiddenAspect;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.cmr.admin.RepoAdminService;
import org.alfresco.service.cmr.dictionary.AspectDefinition;
import org.alfresco.service.cmr.dictionary.ClassDefinition;
import org.alfresco.service.cmr.dictionary.Constraint;
import org.alfresco.service.cmr.dictionary.ConstraintDefinition;
import org.alfresco.service.cmr.dictionary.CustomModelDefinition;
import org.alfresco.service.cmr.dictionary.CustomModelException;
import org.alfresco.service.cmr.dictionary.CustomModelService;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.ModelDefinition;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.service.cmr.dictionary.TypeDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryException.DuplicateDefinitionException;
import org.alfresco.service.cmr.download.DownloadService;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
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.SearchService;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.Pair;
import org.alfresco.util.ParameterCheck;
import org.alfresco.util.PropertyCheck;
import org.alfresco.util.TempFileProvider;
import org.alfresco.util.XMLUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

/**
 * Custom Model Service Implementation
 *
 * @author Jamal Kaabi-Mofrad
 */
public class CustomModelServiceImpl implements CustomModelService
{
    private static final Log logger = LogFactory.getLog(CustomModelServiceImpl.class);

    public static final String DEFAULT_CUSTOM_MODEL_ASPECT = "hasAspect('cmm:customModelManagement')";

    public static final QName ASPECT_CUSTOM_MODEL = QName.createQName("http://www.alfresco.org/model/custommodelmanagement/1.0", "customModelManagement");

    public static final String ALFRESCO_MODEL_ADMINISTRATORS_AUTHORITY = "ALFRESCO_MODEL_ADMINISTRATORS";
    public static final String GROUP_ALFRESCO_MODEL_ADMINISTRATORS_AUTHORITY = PermissionService.GROUP_PREFIX
                + ALFRESCO_MODEL_ADMINISTRATORS_AUTHORITY;

    public static final String SHARE_EXT_MODULE_SUFFIX = "_module.xml";

    /** Messages */
    private static final String MSG_NAME_ALREADY_IN_USE = "cmm.service.name_already_in_use";
    private static final String MSG_CREATE_MODEL_ERR = "cmm.service.create_model_err";
    private static final String MSG_UPDATE_MODEL_ERR = "cmm.service.update_model_err";
    private static final String MSG_MULTIPLE_MODELS = "cmm.service.multiple_models";
    private static final String MSG_RETRIEVE_MODEL = "cmm.service.retrieve_model";
    private static final String MSG_MODEL_NOT_EXISTS = "cmm.service.model_not_exists";
    private static final String MSG_NAMESPACE_NOT_EXISTS = "cmm.service.namespace_not_exists";
    private static final String MSG_NAMESPACE_MANY_EXIST = "cmm.service.namespace_many_exist";
    private static final String MSG_NAMESPACE_URI_ALREADY_IN_USE = "cmm.service.namespace_uri_already_in_use";
    private static final String MSG_NAMESPACE_PREFIX_ALREADY_IN_USE = "cmm.service.namespace_prefix_already_in_use";
    private static final String MSG_UNABLE_DELETE_ACTIVE_MODEL = "cmm.service.unable_delete_active_model";
    private static final String MSG_UNABLE_MODEL_DELETE = "cmm.service.unable_model_delete";
    private static final String MSG_UNABLE_MODEL_DEACTIVATE = "cmm.service.unable_model_deactivate";
    private static final String MSG_UNABLE_MODEL_ACTIVATE = "cmm.service.unable_model_activate";
    private static final String MSG_INVALID_MODEL = "cmm.service.invalid_model";
    private static final String MSG_NAMESPACE_ACTIVE_MODEL = "cmm.service.namespace_active_model";
    private static final String MSG_FAILED_DEACTIVATION_TYPE_DEPENDENCY = "cmm.service.failed.deactivation.type.dependency";
    private static final String MSG_FAILED_DEACTIVATION_ASPECT_DEPENDENCY = "cmm.service.failed.deactivation.aspect.dependency";
    private static final String MSG_DOWNLOAD_COPY_MODEL_ERR = "cmm.service.download.create_model_copy_err";
    private static final String MSG_DOWNLOAD_CREATE_SHARE_EXT_ERR = "cmm.service.download.create_share_ext_err";

    private NodeService nodeService;
    private DictionaryDAOImpl dictionaryDAO;
    private ContentService contentService;
    private SearchService searchService;
    private RepositoryLocation repoModelsLocation;
    private NamespaceDAO namespaceDAO;
    private DictionaryService dictionaryService;
    private RetryingTransactionHelper retryingTransactionHelper;
    private RepoAdminService repoAdminService;
    private AuthorityService authorityService;
    private HiddenAspect hiddenAspect;
    private DownloadService downloadService;
    private DownloadStorage downloadStorage;

    private String shareExtModulePath;


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

    public void setDictionaryDAO(DictionaryDAOImpl dictionaryDAO)
    {
        this.dictionaryDAO = dictionaryDAO;
    }

    public void setContentService(ContentService contentService)
    {
        this.contentService = contentService;
    }

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

    public void setRepositoryModelsLocation(RepositoryLocation repoModelsLocation)
    {
        this.repoModelsLocation = repoModelsLocation;
    }

    public void setDictionaryService(DictionaryService dictionaryService)
    {
        this.dictionaryService = dictionaryService;
    }

    public void setNamespaceDAO(NamespaceDAO namespaceDAO)
    {
        this.namespaceDAO = namespaceDAO;
    }

    public void setRetryingTransactionHelper(RetryingTransactionHelper retryingTransactionHelper)
    {
        this.retryingTransactionHelper = retryingTransactionHelper;
    }

    public void setRepoAdminService(RepoAdminService repoAdminService)
    {
        this.repoAdminService = repoAdminService;
    }

    public void setAuthorityService(AuthorityService authorityService)
    {
        this.authorityService = authorityService;
    }

    public void setHiddenAspect(HiddenAspect hiddenAspect)
    {
        this.hiddenAspect = hiddenAspect;
    }

    public void setDownloadService(DownloadService downloadSerivce)
    {
        this.downloadService = downloadSerivce;
    }

    public void setDownloadStorage(DownloadStorage downloadStorage)
    {
        this.downloadStorage = downloadStorage;
    }

    public void setShareExtModulePath(String shareExtModulePath)
    {
        this.shareExtModulePath = shareExtModulePath;
    }

    /**
     * Checks that all necessary properties and services have been provided.
     */
    public void init()
    {
        PropertyCheck.mandatory(this, "nodeService", nodeService);
        PropertyCheck.mandatory(this, "dictionaryDAO", dictionaryDAO);
        PropertyCheck.mandatory(this, "contentService", contentService);
        PropertyCheck.mandatory(this, "searchService", searchService);
        PropertyCheck.mandatory(this, "repoModelsLocation", repoModelsLocation);
        PropertyCheck.mandatory(this, "namespaceDAO", namespaceDAO);
        PropertyCheck.mandatory(this, "dictionaryService", dictionaryService);
        PropertyCheck.mandatory(this, "retryingTransactionHelper", retryingTransactionHelper);
        PropertyCheck.mandatory(this, "repoAdminService", repoAdminService);
        PropertyCheck.mandatory(this, "authorityService", authorityService);
        PropertyCheck.mandatory(this, "hiddenAspect", hiddenAspect);
        PropertyCheck.mandatory(this, "shareExtModulePath", shareExtModulePath);
        PropertyCheck.mandatory(this, "downloadService", downloadService);
        PropertyCheck.mandatory(this, "downloadStorage", downloadStorage);
    }

    private NodeRef getRootNode()
    {
        StoreRef storeRef = repoModelsLocation.getStoreRef();
        return nodeService.getRootNode(storeRef);
    }

    @Override
    public NodeRef getModelNodeRef(String modelName)
    {
        ParameterCheck.mandatoryString("modelName", modelName);

        StringBuilder builder = new StringBuilder(120);
        builder.append(repoModelsLocation.getPath()).append("//.[@cm:name='").append(modelName).append("' and ")
                    .append(RepoAdminServiceImpl.defaultSubtypeOfDictionaryModel).append(']');

        List<NodeRef> nodeRefs = searchService.selectNodes(getRootNode(), builder.toString(), null, namespaceDAO, false);

        if (nodeRefs.size() == 0)
        {
            return null;
        }
        else if (nodeRefs.size() > 1)
        {
            // unexpected: should not find multiple nodes with same name
            throw new CustomModelException(MSG_MULTIPLE_MODELS, new Object[] { modelName });
        }

        return nodeRefs.get(0);
    }

    private M2Model getM2Model(final NodeRef modelNodeRef)
    {
        ContentReader reader = contentService.getReader(modelNodeRef, ContentModel.PROP_CONTENT);
        if (reader == null)
        {
            return null;
        }
        InputStream in = reader.getContentInputStream();
        try
        {
            return M2Model.createModel(in);
        }
        finally
        {
            if (in != null)
            {
                try
                {
                    in.close();
                }
                catch (IOException e)
                {
                    logger.error("Failed to close input stream for " + modelNodeRef);
                }
            }
        }
    }

    @Override
    public CustomModelDefinition getCustomModel(String modelName)
    {
        ParameterCheck.mandatoryString("modelName", modelName);

        Pair<CompiledModel, Boolean> compiledModelPair = getCustomCompiledModel(modelName);
        CustomModelDefinition result = (compiledModelPair == null) ? null : new CustomModelDefinitionImpl(
                    compiledModelPair.getFirst(), compiledModelPair.getSecond(), dictionaryService);

        return result;
    }

    @Override
    public ModelDefinition getCustomModelByUri(String namespaceUri)
    {
        ParameterCheck.mandatoryString("namespaceUri", namespaceUri);
        CompiledModel compiledModel = getModelByUri(namespaceUri);

        if (compiledModel != null)
        {
            return compiledModel.getModelDefinition();
        }
        return null;
    }

    private CompiledModel getModelByUri(String uri)
    {
        for (CompiledModel model : getAllCustomM2Models(false))
        {
            if (uri.equals(getModelNamespaceUriPrefix(model.getM2Model()).getFirst()))
            {
                return model;
            }
        }
        return null;
    }

    /**
     * Returns compiled custom model and whether the model is active or not as a {@code Pair} object
     *
     * @param modelName the name of the custom model to retrieve
     * @return the {@code Pair<CompiledModel, Boolean>} (or null, if it doesn't exist)
     */
    protected Pair<CompiledModel, Boolean> getCustomCompiledModel(String modelName)
    {
        ParameterCheck.mandatoryString("modelName", modelName);

        final NodeRef modelNodeRef = getModelNodeRef(modelName);

        if (modelNodeRef == null || !nodeService.exists(modelNodeRef))
        {
            return null;
        }

        M2Model model = null;
        final boolean isActive = Boolean.TRUE.equals(nodeService.getProperty(modelNodeRef, ContentModel.PROP_MODEL_ACTIVE));
        if (isActive)
        {
            QName modelQName = (QName) nodeService.getProperty(modelNodeRef, ContentModel.PROP_MODEL_NAME);
            if (modelQName == null)
            {
                return null;
            }
            try
            {
                CompiledModel compiledModel = dictionaryDAO.getCompiledModel(modelQName);
                model = compiledModel.getM2Model();
            }
            catch (Exception e)
            {
                throw new CustomModelException(MSG_RETRIEVE_MODEL, new Object[] { modelName }, e);
            }
        }
        else
        {
            model = getM2Model(modelNodeRef);
        }

        Pair<CompiledModel, Boolean> result = (model == null) ? null : new Pair<>(compileModel(model), isActive);

        return result;
    }

    @Override
    public PagingResults<CustomModelDefinition> getCustomModels(PagingRequest pagingRequest)
    {
        ParameterCheck.mandatory("pagingRequest", pagingRequest);

        List<CustomModelDefinition> result = getAllCustomModels();
        return result.isEmpty() ? new EmptyPagingResults<CustomModelDefinition>() : wrapResult(pagingRequest, result);
    }

    protected List<CustomModelDefinition> getAllCustomModels()
    {
        List<CustomModelDefinition> result = new ArrayList<>();

        Collection<QName> models = dictionaryDAO.getModels(true);

        List<String> dictionaryModels = new ArrayList<>();
        for (QName model : models)
        {
            dictionaryModels.add(model.toPrefixString());
        }

        List<CompiledModel> compiledModels = getAllCustomM2Models(false);
        if (compiledModels.size() > 0)
        {
            for (CompiledModel model : compiledModels)
            {
                // check against models loaded in dictionary
                boolean isActive = false;
                if (dictionaryModels.contains(model.getM2Model().getName()))
                {
                    isActive = true;
                }
                result.add(new CustomModelDefinitionImpl(model, isActive, dictionaryService));
            }
        }

        return result;
    }

    private List<CompiledModel> getAllCustomM2Models(boolean onlyInactiveModels)
    {
        List<CompiledModel> result = new ArrayList<>();

        StringBuilder builder = new StringBuilder(160);
        builder.append(repoModelsLocation.getPath()).append(RepoAdminServiceImpl.CRITERIA_ALL).append("[(")
                    .append(RepoAdminServiceImpl.defaultSubtypeOfDictionaryModel).append(" and ").append(DEFAULT_CUSTOM_MODEL_ASPECT);
        if (onlyInactiveModels)
        {
            builder.append(" and @cm:modelActive='false'");
        }
        builder.append(")]");

        List<NodeRef> nodeRefs = searchService.selectNodes(getRootNode(), builder.toString(), null, namespaceDAO, false,
                    SearchService.LANGUAGE_XPATH);

        if (nodeRefs.size() > 0)
        {
            for (NodeRef nodeRef : nodeRefs)
            {
                try
                {
                    M2Model m2Model = getM2Model(nodeRef);
                    if (m2Model == null)
                    {
                        logger.warn("Couldn't construct M2Model from nodeRef:" + nodeRef);
                        continue;
                    }
                    result.add(compileModel(m2Model));
                }
                catch (Throwable t)
                {
                    logger.warn("Skip model (" + t.getMessage() + ")");
                }
            }
        }

        return result;
    }

    @Override
    public AspectDefinition getCustomAspect(QName name)
    {
        ParameterCheck.mandatory("name", name);

        CompiledModel compiledModel = getModelByUri(name.getNamespaceURI());
        if (compiledModel != null)
        {
            return compiledModel.getAspect(name);
        }

        return null;
    }

    @Override
    public PagingResults<AspectDefinition> getAllCustomAspects(PagingRequest pagingRequest)
    {
        ParameterCheck.mandatory("pagingRequest", pagingRequest);

        List<AspectDefinition> result = new ArrayList<>();
        List<CompiledModel> list = getAllCustomM2Models(false);
        for (CompiledModel model : list)
        {
            result.addAll(model.getAspects());
        }
        return wrapResult(pagingRequest, result);
    }

    @Override
    public TypeDefinition getCustomType(QName name)
    {
        ParameterCheck.mandatory("name", name);

        CompiledModel compiledModel = getModelByUri(name.getNamespaceURI());
        if (compiledModel != null)
        {
            return compiledModel.getType(name);
        }

        return null;
    }

    @Override
    public PagingResults<TypeDefinition> getAllCustomTypes(PagingRequest pagingRequest)
    {
        ParameterCheck.mandatory("pagingRequest", pagingRequest);

        List<TypeDefinition> result = new ArrayList<>();
        List<CompiledModel> list = getAllCustomM2Models(false);
        for (CompiledModel model : list)
        {
            result.addAll(model.getTypes());
        }
        return wrapResult(pagingRequest, result);
    }

    @Override
    public ConstraintDefinition getCustomConstraint(QName name)
    {
        ParameterCheck.mandatory("name", name);

        CompiledModel compiledModel = getModelByUri(name.getNamespaceURI());
        if (compiledModel != null)
        {
            return compiledModel.getConstraint(name);
        }

        return null;
    }

    @Override
    public CustomModelDefinition createCustomModel(M2Model m2Model, boolean activate)
    {
        ParameterCheck.mandatory("m2Model", m2Model);

        String modelName = m2Model.getName();
        int colonIndex = modelName.indexOf(QName.NAMESPACE_PREFIX);
        final String modelFileName = (colonIndex == -1) ? modelName : modelName.substring(colonIndex + 1);

        if (isModelExists(modelFileName))
        {
            throw new CustomModelException.ModelExistsException(MSG_NAME_ALREADY_IN_USE, new Object[] { modelFileName });
        }

        // Validate the model namespace URI
        validateModelNamespaceUri(getModelNamespaceUriPrefix(m2Model).getFirst());
        // Validate the model namespace prefix
        validateModelNamespacePrefix(getModelNamespaceUriPrefix(m2Model).getSecond());

        // Return the created model definition
        CompiledModel compiledModel = createUpdateModel(modelFileName, m2Model, activate, MSG_CREATE_MODEL_ERR, false);
        CustomModelDefinition modelDef = new CustomModelDefinitionImpl(compiledModel, activate, dictionaryService);

        if (logger.isDebugEnabled())
        {
            logger.debug(modelFileName + " model has been created.");
        }
        return modelDef;
    }

    @Override
    public CustomModelDefinition updateCustomModel(String modelFileName, M2Model m2Model, boolean activate)
    {
        ParameterCheck.mandatory("m2Model", m2Model);

        final NodeRef existingModelNodeRef = getModelNodeRef(modelFileName);
        if (existingModelNodeRef == null)
        {
            throw new CustomModelException.ModelDoesNotExistException(MSG_MODEL_NOT_EXISTS, new Object[] { modelFileName });
        }
        // Existing model property and namespace uri-prefix pair
        final boolean isActive = Boolean.TRUE.equals(nodeService.getProperty(existingModelNodeRef, ContentModel.PROP_MODEL_ACTIVE));
        final M2Model existingModel = getM2Model(existingModelNodeRef);
        final Pair<String, String> existingNamespacePair = getModelNamespaceUriPrefix(existingModel);
        // New model namespace uri-prefix pair
        final Pair<String, String> newNamespacePair = getModelNamespaceUriPrefix(m2Model);

        if (isActive && !(existingNamespacePair.equals(newNamespacePair)))
        {
            throw new CustomModelException.ActiveModelConstraintException(MSG_NAMESPACE_ACTIVE_MODEL);
        }

        // if the prefix has changed, then check the new prefix is not in use.
        if (!existingNamespacePair.getSecond().equals(newNamespacePair.getSecond()))
        {
            validateModelNamespacePrefix(newNamespacePair.getSecond());
        }

        // if the URI has changed, then check the new URI is not in use.
        if (!existingNamespacePair.getFirst().equals(newNamespacePair.getFirst()))
        {
            validateModelNamespaceUri(newNamespacePair.getFirst());
        }

        /*
         * We set the requiresNewTx = true, in order to catch any exception
         * thrown within the low level content model management.
         * For example, deleting a property of an active model, where the
         * property has been applied to a node will cause the
         * ModelValidatorImpl to throw an exception.
         * Without starting a new TX, we can't catch that exception.
         */
        CompiledModel compiledModel = createUpdateModel(modelFileName, m2Model, activate, MSG_UPDATE_MODEL_ERR, true);
        CustomModelDefinition modelDef = new CustomModelDefinitionImpl(compiledModel, activate, dictionaryService);

        if (logger.isDebugEnabled())
        {
            logger.debug(modelFileName + " model has been updated.");
        }
        return modelDef;
    }

    private CompiledModel createUpdateModel(final String modelFileName, final M2Model m2Model, final boolean activate, String errMsgId, boolean requiresNewTx)
    {
        // Validate model
        CompiledModel compiledModel = compileModel(m2Model);

        // Validate properties default values
        validatePropsDefaultValues(compiledModel);

        ByteArrayOutputStream xml = new ByteArrayOutputStream();
        m2Model.toXML(xml);
        final InputStream modelStream = new ByteArrayInputStream(xml.toByteArray());

        // Create the model node
        NodeRef nodeRef = doInTransaction(errMsgId, requiresNewTx, new RetryingTransactionCallback<NodeRef>()
        {
            public NodeRef execute() throws Exception
            {
                return repoAdminService.deployModel(modelStream, modelFileName, activate);
            }
        });

        if (!nodeService.hasAspect(nodeRef, ASPECT_CUSTOM_MODEL))
        {
            // Add the 'customModelManagement' marker aspect, to
            // indicate that this model has been created dynamically by this service
            nodeService.addAspect(nodeRef, ASPECT_CUSTOM_MODEL, null);
        }
        // Add hidden aspect
        if (!hiddenAspect.hasHiddenAspect(nodeRef))
        {
            hiddenAspect.hideNode(nodeRef, false, false, false);
        }

        return compiledModel;
    }

    /**
     * Validates the properties' non-null default values against the defined property constraints.
     *
     * @param compiledModel the compiled model
     * @throws CustomModelException.CustomModelConstraintException if there is constraint evaluation
     *                                                             exception
     */
    private void validatePropsDefaultValues(CompiledModel compiledModel)
    {
        for (PropertyDefinition propertyDef : compiledModel.getProperties())
        {
            if (propertyDef.getDefaultValue() != null && propertyDef.getConstraints().size() > 0)
            {
                for (ConstraintDefinition constraintDef : propertyDef.getConstraints())
                {
                    Constraint constraint = constraintDef.getConstraint();
                    try
                    {
                        constraint.evaluate(propertyDef.getDefaultValue());
                    }
                    catch (AlfrescoRuntimeException ex)
                    {
                        String message = getRootCauseMsg(ex, false, "cmm.service.constraint.default_prop_value_err");
                        throw new CustomModelException.CustomModelConstraintException(message);
                    }
                }
            }
        }
    }

    @Override
    public CompiledModel compileModel(M2Model m2Model)
    {
        try
        {
            // Validate model dependencies, constraints and etc. before creating a node
            return m2Model.compile(dictionaryDAO, namespaceDAO, true);
        }
        catch (Exception ex)
        {
            AlfrescoRuntimeException alf = null;
            if (ex instanceof AlfrescoRuntimeException)
            {
                alf = (AlfrescoRuntimeException) ex;
            }
            else
            {
                alf = AlfrescoRuntimeException.create(ex, ex.getMessage());
            }

            Throwable cause = alf.getRootCause();
            String message = null;

            if (cause instanceof DuplicateDefinitionException)
            {
                message = getRootCauseMsg(cause, false, MSG_INVALID_MODEL);
                throw new CustomModelException.CustomModelConstraintException(message);
            }
            else
            {
                message = getRootCauseMsg(cause, true, null);
                throw new CustomModelException.InvalidCustomModelException(MSG_INVALID_MODEL, new Object[] { message }, ex);
            }
        }
    }

    protected <T> PagingResults<T> wrapResult(PagingRequest pagingRequest, List<T> result)
    {
        final int totalSize = result.size();
        final PageDetails pageDetails = PageDetails.getPageDetails(pagingRequest, totalSize);

        final List<T> page = new ArrayList<>(pageDetails.getPageSize());
        Iterator<T> it = result.iterator();
        for (int counter = 0; counter < pageDetails.getEnd() && it.hasNext(); counter++)
        {
            T element = it.next();
            if (counter < pageDetails.getSkipCount())
            {
                continue;
            }
            if (counter > pageDetails.getEnd() - 1)
            {
                break;
            }
            page.add(element);
        }

        return new PagingResults<T>()
        {
            @Override
            public List<T> getPage()
            {
                return page;
            }

            @Override
            public boolean hasMoreItems()
            {
                return pageDetails.hasMoreItems();
            }

            @Override
            public Pair<Integer, Integer> getTotalResultCount()
            {
                Integer total = Integer.valueOf(totalSize);
                return new Pair<>(total, total);
            }

            @Override
            public String getQueryExecutionId()
            {
                return null;
            }
        };
    }

    @Override
    public boolean isModelAdmin(String userName)
    {
        if (userName == null)
        {
            return false;
        }
        return this.authorityService.isAdminAuthority(userName)
                    || this.authorityService.getAuthoritiesForUser(userName).contains(GROUP_ALFRESCO_MODEL_ADMINISTRATORS_AUTHORITY);
    }

    @Override
    public void activateCustomModel(String modelName)
    {
        try
        {
            repoAdminService.activateModel(modelName);
        }
        catch (Exception ex)
        {
            throw new CustomModelException(MSG_UNABLE_MODEL_ACTIVATE, new Object[] { modelName }, ex);
        }
    }

    @Override
    public void deactivateCustomModel(final String modelName)
    {
        CustomModelDefinition customModelDefinition = getCustomModel(modelName);
        if (customModelDefinition == null)
        {
            throw new CustomModelException.ModelDoesNotExistException(MSG_MODEL_NOT_EXISTS, new Object[] { modelName });
        }

        Collection<TypeDefinition> modelTypes = customModelDefinition.getTypeDefinitions();
        Collection<AspectDefinition> modelAspects = customModelDefinition.getAspectDefinitions();

        for (CompiledModel cm : getAllCustomM2Models(false))
        {
            // Ignore type/aspect dependency check within the model itself
            if (!customModelDefinition.getName().equals(cm.getModelDefinition().getName()))
            {
                // Check if the type of the model being deactivated is the parent of another model's type
                validateTypeAspectDependency(modelTypes, cm.getTypes());

                // Check if the aspect of the model being deactivated is the parent of another model's aspect
                validateTypeAspectDependency(modelAspects, cm.getAspects());
            }
        }

        // requiresNewTx = true, in order to catch any exception thrown within
        // "DictionaryModelType$DictionaryModelTypeTransactionListener" model validation.
        doInTransaction(MSG_UNABLE_MODEL_DEACTIVATE, true, new RetryingTransactionCallback<Void>()
        {
            public Void execute() throws Exception
            {
                repoAdminService.deactivateModel(modelName);
                return null;
            }
        });
    }

    private void validateTypeAspectDependency(Collection<? extends ClassDefinition> parentDefs, Collection<? extends ClassDefinition> childDefs)
    {
        for (ClassDefinition parentClassDef : parentDefs)
        {
            for (ClassDefinition childClassDef : childDefs)
            {
                if (parentClassDef.getName().equals(childClassDef.getParentName()))
                {
                    Object[] msgParams = new Object[] { parentClassDef.getName().toPrefixString(),
                                childClassDef.getName().toPrefixString(),
                                childClassDef.getModel().getName().getLocalName() };

                    if (parentClassDef instanceof TypeDefinition)
                    {
                        throw new CustomModelException.CustomModelConstraintException(MSG_FAILED_DEACTIVATION_TYPE_DEPENDENCY, msgParams);
                    }
                    else
                    {
                        throw new CustomModelException.CustomModelConstraintException(MSG_FAILED_DEACTIVATION_ASPECT_DEPENDENCY, msgParams);
                    }
                }
            }
        }
    }

    @Override
    public void deleteCustomModel(String modelName)
    {
        NodeRef nodeRef = getModelNodeRef(modelName);
        if (nodeRef == null)
        {
            throw new CustomModelException.ModelDoesNotExistException(MSG_MODEL_NOT_EXISTS, new Object[] { modelName });
        }

        final boolean isActive = Boolean.TRUE.equals(nodeService.getProperty(nodeRef, ContentModel.PROP_MODEL_ACTIVE));
        if (isActive)
        {
            throw new CustomModelException.ActiveModelConstraintException(MSG_UNABLE_DELETE_ACTIVE_MODEL);
        }
        try
        {
            repoAdminService.undeployModel(modelName);
        }
        catch (Exception ex)
        {
            throw new CustomModelException(MSG_UNABLE_MODEL_DELETE, new Object[] { modelName }, ex);
        }
    }

    @Override
    public boolean isNamespaceUriExists(String modelNamespaceUri)
    {
        ParameterCheck.mandatoryString("modelNamespaceUri", modelNamespaceUri);

        Collection<String> uris = namespaceDAO.getURIs();
        if (uris.contains(modelNamespaceUri))
        {
            return true;
        }

        for (CompiledModel model : getAllCustomM2Models(false))
        {
            if (modelNamespaceUri.equals(getModelNamespaceUriPrefix(model.getM2Model()).getFirst()))
            {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean isNamespacePrefixExists(String modelNamespacePrefix)
    {
        ParameterCheck.mandatoryString("modelNamespacePrefix", modelNamespacePrefix);

        Collection<String> prefixes = namespaceDAO.getPrefixes();
        if (prefixes.contains(modelNamespacePrefix))
        {
            return true;
        }

        for (CompiledModel model : getAllCustomM2Models(false))
        {
            if (modelNamespacePrefix.equals(getModelNamespaceUriPrefix(model.getM2Model()).getSecond()))
            {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean isModelExists(String modelFileName)
    {
        NodeRef nodeRef = getModelNodeRef(modelFileName);
        if (nodeRef != null)
        {
            return true;
        }
        // Also check against the bootstrapped models
        for (QName qname : dictionaryService.getAllModels())
        {
            if (qname.getLocalName().equalsIgnoreCase(modelFileName))
            {
                return true;
            }
        }

        return false;
    }

    private Pair<String, String> getModelNamespaceUriPrefix(M2Model model)
    {
        ParameterCheck.mandatory("model", model);

        List<M2Namespace> namespaces = model.getNamespaces();
        if (namespaces.isEmpty())
        {
            throw new CustomModelException.InvalidNamespaceException(MSG_NAMESPACE_NOT_EXISTS, new Object[] { model.getName() });
        }
        if (namespaces.size() > 1)
        {
            throw new CustomModelException.InvalidNamespaceException(MSG_NAMESPACE_MANY_EXIST, new Object[] { model.getName() });
        }
        M2Namespace ns = namespaces.iterator().next();

        return new Pair<>(ns.getUri(), ns.getPrefix());
    }

    private void validateModelNamespaceUri(String uri)
    {
        if (isNamespaceUriExists(uri))
        {
            throw new CustomModelException.NamespaceConstraintException(MSG_NAMESPACE_URI_ALREADY_IN_USE, new Object[] { uri });
        }
    }

    private void validateModelNamespacePrefix(String prefix)
    {
        if (isNamespacePrefixExists(prefix))
        {
            throw new CustomModelException.NamespaceConstraintException(MSG_NAMESPACE_PREFIX_ALREADY_IN_USE, new Object[] { prefix });
        }
    }

    /**
     * A helper method to run a unit of work in a transaction.
     *
     * @param errMsgId message id for the new wrapper exception ({@link CustomModelException})
     *            when an exception occurs
     * @param requiresNewTx <tt>true</tt> to force a new transaction or
     *            <tt>false</tt> to partake in any existing transaction
     * @param cb The callback containing the unit of work
     * @return Returns the result of the unit of work
     */
    private <R> R doInTransaction(String errMsgId, boolean requiresNewTx, RetryingTransactionCallback<R> cb)
    {
        try
        {
            return retryingTransactionHelper.doInTransaction(cb, false, requiresNewTx);
        }
        catch (Exception ex)
        {
            AlfrescoRuntimeException alf = null;
            if (ex instanceof AlfrescoRuntimeException)
            {
                alf = (AlfrescoRuntimeException) ex;
            }
            else
            {
                alf = AlfrescoRuntimeException.create(ex, ex.getMessage());
            }

            Throwable cause = alf.getRootCause();
            String message = getRootCauseMsg(cause, true, null);

            throw new CustomModelException(errMsgId, new Object[] { message }, ex);
        }
    }

    private static String getRootCauseMsg(Throwable cause, boolean withAlfLogNum, String defaultMsg)
    {
        if (defaultMsg == null)
        {
            defaultMsg = "";
        }

        String message = cause.getMessage();
        if(message == null)
        {
            return defaultMsg;
        }
        else
        {
            return ((withAlfLogNum) ? message : message.replaceFirst("\\d+", "").trim());
        }
    }

    @Override
    public NodeRef createDownloadNode(final String modelFileName, boolean withAssociatedForm)
    {
        List<NodeRef> nodesToBeDownloaded = new ArrayList<>(2);

        NodeRef customModelNodeRef = getModelNodeRef(modelFileName);
        if(customModelNodeRef == null)
        {
            throw new CustomModelException.ModelDoesNotExistException(MSG_MODEL_NOT_EXISTS, new Object[] { modelFileName });
        }
        // We create a copy of the model, so we can rename it, change its
        // content type and move it to the download container in order to be
        // cleaned by the download service cleanup job.
        customModelNodeRef = createCustomModelCopy(modelFileName + ".xml", customModelNodeRef);
        nodesToBeDownloaded.add(customModelNodeRef);

        if (withAssociatedForm)
        {
            NodeRef shareExtModuleNodeRef = null;
            try
            {
                shareExtModuleNodeRef = createCustomModelShareExtModuleRef(modelFileName);
                nodesToBeDownloaded.add(shareExtModuleNodeRef);

                if (logger.isDebugEnabled())
                {
                    StringBuilder msg = new StringBuilder();
                    msg.append("Temp nodes created for download: Custom model nodeRef [")
                                .append(customModelNodeRef)
                                .append("] and its associated Share form nodeRef [")
                                .append(shareExtModuleNodeRef).append(']');
                    logger.debug(msg.toString());
                }
            }
            catch (CustomModelException ex)
            {
                // We don't throw the exception as the Model might be a
                // draft model and might have never been activated or never had any forms created for it.
                // So in this case we just construct the zip containing only the model.
                StringBuilder msg = new StringBuilder();
                msg.append("Constructing CMM zip file containing only the model [")
                            .append(modelFileName)
                            .append(".xml] without its associated share extension module, because: ")
                            .append(ex.getMessage());

                logger.warn(msg.toString());
            }

        }
        else
        {
            if (logger.isDebugEnabled())
            {
                StringBuilder msg = new StringBuilder();
                msg.append("Temp node created for download: Custom model nodeRef [")
                            .append(customModelNodeRef).append(']');
                logger.debug(msg.toString());
            }
        }

        try
        {
            NodeRef archiveNodeRef = downloadService.createDownload(nodesToBeDownloaded.toArray(new NodeRef[nodesToBeDownloaded.size()]), false);

            if (logger.isDebugEnabled())
            {
                StringBuilder msg = new StringBuilder();
                msg.append("Created download nodeRef [").append(archiveNodeRef).append(']');
                logger.debug(msg.toString());
            }

            return archiveNodeRef;
        }
        catch (Exception ex)
        {
            throw new CustomModelException("cmm.service.download.create_err", ex);
        }
    }

    /**
     * Finds the {@code module} element within the Share persisted-extension
     * XML file and then writes the XML fragment as the content of a newly created node.
     *
     * @param modelName the model name
     * @return the created nodeRef
     */
    protected NodeRef createCustomModelShareExtModuleRef(final String modelName)
    {
        final String moduleId = "CMM_" + modelName;

        final NodeRef formNodeRef = getShareExtModule();
        ContentReader reader = contentService.getReader(formNodeRef, ContentModel.PROP_CONTENT);

        if (reader == null)
        {
            throw new CustomModelException("cmm.service.download.share_ext_node_read_err");
        }

        InputStream in = reader.getContentInputStream();
        Node moduleIdXmlNode = null;
        try
        {
            Document document = XMLUtil.parse(in); // the stream will be closed

            final String xpathQuery = "/extension//modules//module//id[.= '" + moduleId + "']";

            XPath xPath = XPathFactory.newInstance().newXPath();
            XPathExpression expression = xPath.compile(xpathQuery);

            moduleIdXmlNode = (Node) expression.evaluate(document, XPathConstants.NODE);
        }
        catch (Exception ex)
        {
            throw new CustomModelException("cmm.service.download.share_ext_file_parse_err", ex);
        }

        if (moduleIdXmlNode == null)
        {
            throw new CustomModelException("cmm.service.download.share_ext_module_not_found", new Object[] { moduleId });
        }

        final File moduleFile = TempFileProvider.createTempFile(moduleId, ".xml");
        try
        {
            XMLUtil.print(moduleIdXmlNode.getParentNode(), moduleFile);
        }
        catch (IOException error)
        {
            throw new CustomModelException("cmm.service.download.share_ext_write_err", new Object[] { moduleId }, error);
        }

        return doInTransaction(MSG_DOWNLOAD_CREATE_SHARE_EXT_ERR, true, new RetryingTransactionCallback<NodeRef>()
        {
            @Override
            public NodeRef execute() throws Exception
            {
                final NodeRef nodeRef = createDownloadTypeNode(moduleId + SHARE_EXT_MODULE_SUFFIX);
                ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true);
                writer.setMimetype(MimetypeMap.MIMETYPE_XML);
                writer.setEncoding("UTF-8");
                writer.putContent(moduleFile);

                return nodeRef;
            }
        });
    }

    /**
     * Gets Share persisted-extension nodeRef
     */
    protected NodeRef getShareExtModule()
    {
        List<NodeRef> results = searchService.selectNodes(getRootNode(), this.shareExtModulePath, null, this.namespaceDAO, false,
                    SearchService.LANGUAGE_XPATH);

        if (results.isEmpty())
        {
            throw new CustomModelException("cmm.service.download.share_ext_file_not_found");
        }

        return results.get(0);
    }

    /**
     * Creates a copy of the custom model where the created node will be a child
     * of download container.
     *
     * @param newName the model new name
     * @param modelNodeRef existing model nodeRef
     * @return the created nodeRef
     */
    protected NodeRef createCustomModelCopy(final String newName, final NodeRef modelNodeRef)
    {
        return doInTransaction(MSG_DOWNLOAD_COPY_MODEL_ERR, true, new RetryingTransactionCallback<NodeRef>()
        {
            @Override
            public NodeRef execute() throws Exception
            {
                final NodeRef newNodeRef = createDownloadTypeNode(newName);
                Serializable content = nodeService.getProperty(modelNodeRef, ContentModel.PROP_CONTENT);
                nodeService.setProperty(newNodeRef, ContentModel.PROP_CONTENT, content);

               return newNodeRef;
            }
        });
    }

    /**
     * Creates node with a type {@link DownloadModel#TYPE_DOWNLOAD} within the
     * download container (see
     * {@link DownloadStorage#getOrCreateDowloadContainer()} )
     * <p>
     * Also, the {@code IndexControlAspect} is applied to the created node.
     *
     * @param name the node name
     * @return the created nodeRef
     */
    private NodeRef createDownloadTypeNode(final String name)
    {
        final NodeRef newNodeRef = nodeService.createNode(
                    downloadStorage.getOrCreateDowloadContainer(),
                    ContentModel.ASSOC_CHILDREN,
                    ContentModel.ASSOC_CHILDREN,
                    DownloadModel.TYPE_DOWNLOAD,
                    Collections.<QName, Serializable> singletonMap(ContentModel.PROP_NAME, name)).getChildRef();

        Map<QName, Serializable> aspectProperties = new HashMap<>(2);
        aspectProperties.put(ContentModel.PROP_IS_INDEXED, Boolean.FALSE);
        aspectProperties.put(ContentModel.PROP_IS_CONTENT_INDEXED, Boolean.FALSE);
        nodeService.addAspect(newNodeRef, ContentModel.ASPECT_INDEX_CONTROL, aspectProperties);

        return newNodeRef;
    }

    @Override
    public CustomModelsInfo getCustomModelsInfo()
    {
        List<CustomModelDefinition> page = getCustomModels(new PagingRequest(0, Integer.MAX_VALUE)).getPage();

        int activeModels = 0;
        int activeTypes = 0;
        int activeAspects = 0;
        for (CustomModelDefinition cm : page)
        {
            if (cm.isActive())
            {
                activeModels++;
                activeTypes += cm.getTypeDefinitions().size();
                activeAspects += cm.getAspectDefinitions().size();
            }
        }

        CustomModelsInfo info = new CustomModelsInfo();
        info.setNumberOfActiveModels(activeModels);
        info.setNumberOfActiveTypes(activeTypes);
        info.setNumberOfActiveAspects(activeAspects);
        return info;
    }
}
