/*
 * #%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 .
 * #L%
 */
package org.alfresco.repo.admin;
import java.io.InputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ApplicationModel;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.dictionary.DictionaryDAO;
import org.alfresco.repo.dictionary.M2Model;
import org.alfresco.repo.dictionary.RepositoryLocation;
import org.alfresco.repo.i18n.MessageService;
import org.alfresco.repo.i18n.MessageServiceImpl;
import org.alfresco.repo.usage.RepoUsageComponent;
import org.alfresco.service.cmr.admin.RepoAdminService;
import org.alfresco.service.cmr.admin.RepoUsage;
import org.alfresco.service.cmr.admin.RepoUsage.UsageType;
import org.alfresco.service.cmr.admin.RepoUsageStatus;
import org.alfresco.service.cmr.dictionary.DictionaryException;
import org.alfresco.service.cmr.dictionary.ModelDefinition;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
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.DuplicateChildNodeNameException;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
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.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.extensions.surf.util.I18NUtil;
import org.springframework.extensions.surf.util.ParameterCheck;
/**
 * Repository Admin Service Implementation.
 * 
 * @see RepoAdminService interface
 */
public class RepoAdminServiceImpl implements RepoAdminService
{   
    // Logging support
    private static Log logger = LogFactory.getLog(RepoAdminServiceImpl.class);
    
    // dependencies  
    private DictionaryDAO dictionaryDAO;
    private SearchService searchService;
    private NodeService nodeService;
    private ContentService contentService;
    private NamespaceService namespaceService;
    private MessageService messageService;
    private RepoUsageComponent repoUsageComponent;
    
    private RepositoryLocation repoModelsLocation;
    private RepositoryLocation repoMessagesLocation;
    
    public final static String CRITERIA_ALL = "/*"; // immediate children only
   
    public final static String defaultSubtypeOfDictionaryModel = "subtypeOf('cm:dictionaryModel')";
    public final static String defaultSubtypeOfContent = "subtypeOf('cm:content')";
    private static final String MODELS_LOCATION_NOT_FOUND = "repoadmin_service.models_location_not_found";
    private static final String MODELS_LOCATION_MULTIPLE_FOUND = "repoadmin_service.models_location_multiple_found";
    private static final String MODEL_EXISTS = "repoadmin_service.model_exists";
    private static final String MODEL_DEPLOYMENT_FAILED = "repoadmin_service.model_deployment_failed";
    private static final String MODEL_UNDEPLOYMENT_FAILED = "repoadmin_service.model_undeployment_failed";
    private static final String MODEL_ACTIVATION_FAILED = "repoadmin_service.model_activation_failed";
    private static final String MODEL_DEACTIVATION_FAILED = "repoadmin_service.model_deactivation_failed";
    private static final String MODEL_NOT_FOUND = "repoadmin_service.model_not_found";
    private static final String MODELS_MULTIPLE_FOUND = "repoadmin_service.models_multiple_found";
    private static final String MODEL_ALREADY_ACTIVATED = "repoadmin_service.model_already_activated";
    private static final String MODEL_ALREADY_DEACTIVATED = "repoadmin_service.model_already_deactivated";
    private static final String MODEL_NO_LONGER_EXISTS = "repoadmin_service.model_no_longer_exists";
    private static final String MSG_RESOURCES_NOT_FOUND = "repoadmin_service.msg_resource_not_found";
    private static final String MSG_RESOURCES_DEPLOYMENT_FAILED = "repoadmin_service.msg_resource_deployment_failed";
    private static final String MESSAGES_LOCATION_NOT_FOUND = "repoadmin_service.messages_location_not_found";
    private static final String MESSAGES_LOCATION_MULTIPLE_FOUND = "repoadmin_service.messages_location_multiple_found";
    private static final String MSG_RESOURCES_UNDEPLOYMENT_FAILED = "repoadmin_service.msg_resource_undeployment_failed";
    private static final String MSG_RESOURCES_RELOAD_FAILED = "repoadmin_service.msg_resource_reload_failed";
    private static final String MSG_MISSING_BUNDLE_BASE_NAME = "repoadmin_service.msg_missing_bundle_base_name";
    private static final String MSG_BASE_NAME_CONTAIN_UNDERSCORE = "repoadmin_service.msg_base_name_contain_underscore";
    private static final String MSG_BASE_NAME_CONTAIN_PERIOD = "repoadmin_service.msg_base_name_contain_period";
    public void setDictionaryDAO(DictionaryDAO dictionaryDAO)
    {
        this.dictionaryDAO = dictionaryDAO;
    }
    
    public void setSearchService(SearchService searchService)
    {
        this.searchService = searchService;
    }
 
    public void setNodeService(NodeService nodeService)
    {
        this.nodeService = nodeService;
    }
    
    public void setContentService(ContentService contentService)
    {
        this.contentService = contentService;
    }
    
    public void setNamespaceService(NamespaceService namespaceService)
    {
        this.namespaceService = namespaceService;
    }
    
    public void setMessageService(MessageService messageService)
    {
        this.messageService = messageService;
    }
    public void setRepoUsageComponent(RepoUsageComponent repoUsageComponent)
    {
        this.repoUsageComponent = repoUsageComponent;
    }
    public void setRepositoryModelsLocation(RepositoryLocation repoModelsLocation)
    {
        this.repoModelsLocation = repoModelsLocation;
    }
    
    public void setRepositoryMessagesLocation(RepositoryLocation repoMessagesLocation)
    {
        this.repoMessagesLocation = repoMessagesLocation;
    }
    
    public List getModels()
    {
        StoreRef storeRef = repoModelsLocation.getStoreRef();
        NodeRef rootNode = nodeService.getRootNode(storeRef);
        
        List modelsInRepo = new ArrayList();
        
        Collection models = dictionaryDAO.getModels(true);  
        
        List dictionaryModels = new ArrayList();
        for (QName model : models)
        {
            dictionaryModels.add(model.toPrefixString());
        }
        
        List nodeRefs = searchService.selectNodes(rootNode, repoModelsLocation.getPath()+CRITERIA_ALL+"["+defaultSubtypeOfDictionaryModel+"]", null, namespaceService, false);
        
        if (nodeRefs.size() > 0)
        {
            for (NodeRef nodeRef : nodeRefs)
            {
                String modelFileName = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_NAME);
                String repoVersion = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_VERSION_LABEL);
                
                try
                {
                    String modelName = null;
                    
                    ContentReader cr = contentService.getReader(nodeRef, ContentModel.TYPE_CONTENT);
                    
                    if (cr != null)
                    {
                        InputStream is = cr.getContentInputStream();
                        try
                        {
                            M2Model model = M2Model.createModel(is);
                            modelName = model.getName();
                        }
                        finally
                        {
                            is.close();
                        }
                    }
                    
                    // check against models loaded in dictionary and give warning if not found
                    if (dictionaryModels.contains(modelName))
                    {
                        // note: uses dictionary model cache, rather than getting content from repo and re-compiling
                        modelsInRepo.add(new RepoModelDefinition(modelFileName, repoVersion, dictionaryDAO.getModel(QName.createQName(modelName, namespaceService)), true));
                    }
                    else
                    {
                        modelsInRepo.add(new RepoModelDefinition(modelFileName, repoVersion, null, false));
                    }
                }
                catch (Throwable t)
                {
                    logger.warn("Skip model: "+modelFileName+" ("+t.getMessage()+")");
                }
            }
        }
        
        return modelsInRepo;
    }
    @Override
    public NodeRef deployModel(InputStream modelStream, String modelFileName, boolean activate)
    {
        try
        {   
            // Check that all the passed values are not null
            ParameterCheck.mandatory("ModelStream", modelStream);
            ParameterCheck.mandatoryString("ModelFileName", modelFileName);
            
            Map contentProps = new HashMap();
            contentProps.put(ContentModel.PROP_NAME, modelFileName);
            
            StoreRef storeRef = repoModelsLocation.getStoreRef();
            NodeRef rootNode = nodeService.getRootNode(storeRef);
            
            List nodeRefs = searchService.selectNodes(rootNode, repoModelsLocation.getPath(), null, namespaceService, false);
            
            if (nodeRefs.size() == 0)
            {
                throw new AlfrescoRuntimeException(MODELS_LOCATION_NOT_FOUND, new Object[] { repoModelsLocation.getPath() });
            }
            else if (nodeRefs.size() > 1)
            {
                // unexpected: should not find multiple nodes with same name
                throw new AlfrescoRuntimeException(MODELS_LOCATION_MULTIPLE_FOUND, new Object[] { repoModelsLocation.getPath() });
            }
            
            NodeRef customModelsSpaceNodeRef = nodeRefs.get(0);
            
            nodeRefs = searchService.selectNodes(customModelsSpaceNodeRef, "*[@cm:name='"+modelFileName+"' and "+defaultSubtypeOfDictionaryModel+"]", null, namespaceService, false);
            
            NodeRef modelNodeRef = null;
                
            if (nodeRefs.size() == 1)
            {
                // re-deploy existing model to the repository       
                
                modelNodeRef = nodeRefs.get(0);
            }
            else
            {
                // deploy new model to the repository
                
                try
                {
                    // note: dictionary model type has associated policies that will be invoked
                    ChildAssociationRef association = nodeService.createNode(customModelsSpaceNodeRef,
                            ContentModel.ASSOC_CONTAINS,
                            QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, modelFileName),
                            ContentModel.TYPE_DICTIONARY_MODEL,
                            contentProps); // also invokes policies for DictionaryModelType - e.g. onUpdateProperties
                                
                    modelNodeRef = association.getChildRef();
                }
                catch (DuplicateChildNodeNameException dcnne)
                {
                    String msg = "Model already exists: "+modelFileName+" - "+dcnne;
                    logger.warn(msg);
                    // for now, assume concurrency failure
                    throw new ConcurrencyFailureException(getLocalisedMessage(MODEL_EXISTS, modelFileName));
                }
                
                // add titled aspect (for Web Client display)
                Map titledProps = new HashMap();
                titledProps.put(ContentModel.PROP_TITLE, modelFileName);
                titledProps.put(ContentModel.PROP_DESCRIPTION, modelFileName);
                nodeService.addAspect(modelNodeRef, ContentModel.ASPECT_TITLED, titledProps);
                
                // add versionable aspect (set auto-version)
                Map versionProps = new HashMap();
                versionProps.put(ContentModel.PROP_AUTO_VERSION, true);
                nodeService.addAspect(modelNodeRef, ContentModel.ASPECT_VERSIONABLE, versionProps);
            }
            
            ContentWriter writer = contentService.getWriter(modelNodeRef, ContentModel.PROP_CONTENT, true);
            writer.setMimetype(MimetypeMap.MIMETYPE_XML);
            writer.setEncoding("UTF-8");
            
            writer.putContent(modelStream); // also invokes policies for DictionaryModelType - e.g. onContentUpdate
            modelStream.close();
            
            // activate the model
            nodeService.setProperty(modelNodeRef, ContentModel.PROP_MODEL_ACTIVE, Boolean.valueOf(activate));
            
            // note: model will be loaded as part of DictionaryModelType.beforeCommit()
            return modelNodeRef;
        }
        catch (Throwable e)
        {
            throw new AlfrescoRuntimeException(MODEL_DEPLOYMENT_FAILED, e);
        }     
    }
    public void deployModel(InputStream modelStream, String modelFileName)
    {
        deployModel(modelStream, modelFileName, true);
    }
    public QName activateModel(String modelFileName)
    {     
        try
        {
        	return activateOrDeactivate(modelFileName, true);
        }
        catch (Throwable e)
        {
            throw new AlfrescoRuntimeException(MODEL_ACTIVATION_FAILED, e);
        }
    }  
    
    public QName deactivateModel(String modelFileName)
    { 
        try
        {
        	return activateOrDeactivate(modelFileName, false);
        }
        catch (Throwable e)
        {
            throw new AlfrescoRuntimeException(MODEL_DEACTIVATION_FAILED, e);
        }
    }
    
    private QName activateOrDeactivate(String modelFileName, boolean activate)
    {
        // Check that all the passed values are not null        
        ParameterCheck.mandatoryString("modelFileName", modelFileName);
        StoreRef storeRef = repoModelsLocation.getStoreRef();          
        NodeRef rootNode = nodeService.getRootNode(storeRef);
        
        List nodeRefs = searchService.selectNodes(rootNode, repoModelsLocation.getPath()+"//.[@cm:name='"+modelFileName+"' and "+defaultSubtypeOfDictionaryModel+"]", null, namespaceService, false);
        
        if (nodeRefs.size() == 0)
        {
            throw new AlfrescoRuntimeException(MODEL_NOT_FOUND, new Object[] { modelFileName });
        }
        else if (nodeRefs.size() > 1)
        {
            // unexpected: should not find multiple nodes with same name
            throw new AlfrescoRuntimeException(MODELS_MULTIPLE_FOUND, new Object[] { modelFileName });
        }
        
        NodeRef modelNodeRef = nodeRefs.get(0);
        
        boolean isActive = false;
        Boolean value = (Boolean)nodeService.getProperty(modelNodeRef, ContentModel.PROP_MODEL_ACTIVE);
        if (value != null)
        {
            isActive = value.booleanValue();
        }
        
        QName modelQName = (QName)nodeService.getProperty(modelNodeRef, ContentModel.PROP_MODEL_NAME);
        
        ModelDefinition modelDef = null;
        if (modelQName != null)
        {
	        try
	        {
	        	modelDef = dictionaryDAO.getModel(modelQName);
	        }
	        catch (DictionaryException e)
	        {
	        	logger.warn(e);
	        }
        }
        
        if (activate) 
        {
        	// activate
        	if (isActive)
        	{
	        	if (modelDef != null)
	        	{
	        		// model is already activated
                    throw new AlfrescoRuntimeException(MODEL_ALREADY_ACTIVATED, new Object[] { modelQName });
	        	}
	        	else
	        	{
	        		logger.warn("Model is set to active but not loaded in Dictionary - trying to load...");
	        	}
        	}
        	else
        	{
	        	if (modelDef != null)
	        	{
	        		logger.warn("Model is loaded in Dictionary but is not set to active - trying to activate...");
	        	}
        	}
        }
        else
        {
        	// deactivate
        	if (!isActive)
        	{
	        	if (modelDef == null)
	        	{
	        		// model is already deactivated
                    throw new AlfrescoRuntimeException(MODEL_ALREADY_DEACTIVATED, new Object[] { modelQName });
	        	}
	        	else
	        	{
	        		logger.warn("Model is set to inactive but loaded in Dictionary - trying to unload...");
	        	}
        	}
        	else
        	{
	        	if (modelDef == null)
	        	{
	        		logger.warn("Model is not loaded in Dictionary but is set to active - trying to deactivate...");
	        	}
        	}
        }
         
        // activate/deactivate the model 
        nodeService.setProperty(modelNodeRef, ContentModel.PROP_MODEL_ACTIVE, new Boolean(activate));
        
        // note: model will be loaded/unloaded as part of DictionaryModelType.beforeCommit()
        return modelQName;
    }  
    public QName undeployModel(String modelFileName)
    {
        // Check that all the passed values are not null
        ParameterCheck.mandatory("modelFileName", modelFileName);
        
        QName modelQName = null;
        
        try
        { 
            // find model in repository
            
            StoreRef storeRef = repoModelsLocation.getStoreRef();
            NodeRef rootNode = nodeService.getRootNode(storeRef);
            
            List nodeRefs = null;
            try
            {
                nodeRefs = searchService.selectNodes(rootNode, repoModelsLocation.getPath()+"//.[@cm:name='"+modelFileName+"' and "+defaultSubtypeOfDictionaryModel+"]", null, namespaceService, false);
            }
            catch (InvalidNodeRefException inre)
            {
                String msg = "Model no longer exists: "+modelFileName+" - "+inre;
                logger.warn(msg);
                // for now, assume concurrency failure
                throw new ConcurrencyFailureException(getLocalisedMessage(MODEL_NO_LONGER_EXISTS, modelFileName));
            }
            
            if (nodeRefs.size() == 0)
            {
                throw new AlfrescoRuntimeException(MODEL_NOT_FOUND, new Object[] { modelFileName });
            }
            else if (nodeRefs.size() > 1)
            {
                // unexpected: should not find multiple nodes with same name
                throw new AlfrescoRuntimeException(MODELS_MULTIPLE_FOUND, new Object[] { modelFileName });
            }
            
            NodeRef modelNodeRef = nodeRefs.get(0);
            
            boolean isActive = false;
            Boolean value = (Boolean)nodeService.getProperty(modelNodeRef, ContentModel.PROP_MODEL_ACTIVE);
            if (value != null)
            {
                isActive = value.booleanValue();
            }
            
            modelQName = (QName)nodeService.getProperty(modelNodeRef, ContentModel.PROP_MODEL_NAME);
            
            ModelDefinition modelDef = null;
            if (modelQName != null)
            {
	            try
	            {
	            	modelDef = dictionaryDAO.getModel(modelQName);
	            }
	            catch (DictionaryException e)
	            {
	            	logger.warn(e);
	            }
            }
 
        	if (isActive)
        	{
        		if (modelDef == null)
        		{
        			logger.warn("Model is set to active but not loaded in Dictionary - trying to undeploy...");
        		}
            }
        	
            // permanently remove model from repository
            nodeService.addAspect(modelNodeRef, ContentModel.ASPECT_TEMPORARY, null);
            
            try
            {
                nodeService.deleteNode(modelNodeRef);
            }
            catch (DictionaryException de)
            {
                String msg = "Model undeployment failed: "+modelFileName+" - "+de;
                logger.warn(msg);
                // for now, assume concurrency failure
                throw new ConcurrencyFailureException(msg);
            }
            
            // note: deleted model will be unloaded as part of DictionaryModelType.beforeCommit()
        }
        catch (Throwable e)
        {
            throw new AlfrescoRuntimeException(MODEL_UNDEPLOYMENT_FAILED, e);
        }
        
        return modelQName;
    }
    public List getMessageBundles()
    {
        StoreRef storeRef = repoMessagesLocation.getStoreRef(); 
        NodeRef rootNode = nodeService.getRootNode(storeRef);
        
        Collection registeredBundles = messageService.getRegisteredBundles();
        List nodeRefs = searchService.selectNodes(rootNode, repoMessagesLocation.getPath()+CRITERIA_ALL+"["+defaultSubtypeOfContent+"]", null, namespaceService, false);
                
        List resourceBundlesInRepo = new ArrayList(); 
        
        for (NodeRef nodeRef : nodeRefs)
        {
            String resourceName = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_NAME);
            String resourceBundleBaseName = null;
            int idx1 = resourceName.indexOf("_");
            if (idx1 > 0)
            {
                resourceBundleBaseName = resourceName.substring(0, idx1);
            }
            else
            {
                int idx2 = resourceName.indexOf(".");
                if (idx2 > 0)
                {
                    resourceBundleBaseName = resourceName.substring(0, idx2);
                }
                else
                {
                    // Unexpected format
                    logger.warn("Unexpected message resource name: " + resourceName);
              }
            }
            if (registeredBundles != null) 
            {
                for (String registeredBundlePath : registeredBundles)
                {
                    if (registeredBundlePath.endsWith(resourceBundleBaseName) && (! resourceBundlesInRepo.contains(resourceBundleBaseName)))
                    {
                        resourceBundlesInRepo.add(resourceBundleBaseName);
                    }
                }
            }
            else
            {
                // unexpected
                logger.error("Message bundle not registered: " + resourceBundleBaseName);
            }
        }
        
        return resourceBundlesInRepo;   
    }
    
    public String deployMessageBundle(String resourceClasspath)
    {   
        // Check that all the passed values are not null        
        ParameterCheck.mandatory("ResourceClasspath", resourceClasspath);
        
        String bundleBaseName = resourceClasspath;
        
        // note: resource path should be in form path1/path2/path3/bundlebasename
        int idx = resourceClasspath.lastIndexOf("/");
        
        if (idx != -1)
        {
            if (idx < (resourceClasspath.length()-1))
            {
                bundleBaseName = resourceClasspath.substring(idx+1);
            }
            else
            {
                bundleBaseName = null;
            }
        }
        
        checkBundleBaseName(bundleBaseName);
        
        String pattern = "classpath*:" + resourceClasspath + "*" + MessageServiceImpl.PROPERTIES_FILE_SUFFIX;
        
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
       
        try
        {
            Resource[] resources = resolver.getResources(pattern);
            
            if ((resources != null) && (resources.length > 0))
            {
                ArrayList names = new ArrayList();
                ArrayList filteredResources = new ArrayList();
                
                for (int i = 0; i < resources.length; i++)
                {
                    String filename = resources[i].getFilename();
                    if (! names.contains(filename))
                    {
                        names.add(filename);
                        filteredResources.add(resources[i]);
                    }
                }
            
                for (Resource resource : filteredResources)
                {
                    InputStream fileStream = resource.getInputStream();
                    String filename = resource.getFilename();
                    deployMessageResourceFile(resourceClasspath, filename, fileStream, false);   
                }           
                
                // register bundle
                
                StoreRef storeRef = repoMessagesLocation.getStoreRef();
                String repoBundlePath = storeRef.toString() + repoMessagesLocation.getPath() + "/cm:" + bundleBaseName;
                messageService.registerResourceBundle(repoBundlePath); 
                
                logger.info("Message resource bundle deployed: " + bundleBaseName);
            }
            else
            {
                logger.warn("No message resources found: " + resourceClasspath);
                throw new AlfrescoRuntimeException(MSG_RESOURCES_NOT_FOUND, new Object[] { resourceClasspath });
            }
        }
        catch (Throwable e)
        {
            throw new AlfrescoRuntimeException(MSG_RESOURCES_DEPLOYMENT_FAILED, new Object[] { resourceClasspath }, e);
        }
        
        return bundleBaseName;
    }
    
    /*
     * Deploy message resource file
     */
    private void deployMessageResourceFile(String bundleBasePath, String name, InputStream resourceStream, boolean registerResourceBundle)
    {    
        // Check that all the passed values are not null
        ParameterCheck.mandatory("BundleBasePath", bundleBasePath);
        ParameterCheck.mandatory("Name", name);
        ParameterCheck.mandatory("ResourceStream", resourceStream);
        
        try
        {        
            Map contentProps = new HashMap();
            contentProps.put(ContentModel.PROP_NAME, name);
            
            StoreRef storeRef = repoMessagesLocation.getStoreRef();
            NodeRef rootNode = nodeService.getRootNode(storeRef);
            
            List nodeRefs = searchService.selectNodes(rootNode, repoMessagesLocation.getPath(), null, namespaceService, false);
            
            if (nodeRefs.size() == 0)
            {
                throw new AlfrescoRuntimeException(MESSAGES_LOCATION_NOT_FOUND, new Object[] { repoMessagesLocation.getPath() });
            }
            else if (nodeRefs.size() > 1)
            {
                // unexpected: should not find multiple nodes with same name                
                throw new AlfrescoRuntimeException(MESSAGES_LOCATION_MULTIPLE_FOUND, new Object[] { repoMessagesLocation.getPath() });
            }
            NodeRef customLabelsNodeRef = nodeRefs.get(0);
                                   
            ChildAssociationRef association = nodeService.createNode(customLabelsNodeRef, 
                    ContentModel.ASSOC_CONTAINS, 
                    QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, name),
                    ContentModel.TYPE_CONTENT,
                    contentProps);
            
            NodeRef content = association.getChildRef();
            
            // add titled aspect (for Web Client display)
            Map titledProps = new HashMap();
            titledProps.put(ContentModel.PROP_TITLE, name);
            titledProps.put(ContentModel.PROP_DESCRIPTION, name);
            nodeService.addAspect(content, ContentModel.ASPECT_TITLED, titledProps);
            
            // add inline-editable aspect
            Map editProps = new HashMap(1, 1.0f);
            editProps.put(ApplicationModel.PROP_EDITINLINE, true);
            nodeService.addAspect(content, ApplicationModel.ASPECT_INLINEEDITABLE, editProps);
               
            ContentWriter writer = contentService.getWriter(content, ContentModel.PROP_CONTENT, true);
            writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
            writer.setEncoding("UTF-8");
        
            writer.putContent(resourceStream);
            resourceStream.close();
            
            if (registerResourceBundle == true)
            {
                String bundleBaseName = null;
                int idx = bundleBasePath.lastIndexOf("/");
                if ((idx != -1) && (idx != bundleBasePath.length() - 1))
                {
                    bundleBaseName = bundleBasePath.substring(idx+1);
                }
                else
                {
                    bundleBaseName = bundleBasePath;
                }
                
                String repoBundlePath = storeRef.toString() + repoMessagesLocation.getPath() + "/cm:" + bundleBaseName;
                messageService.registerResourceBundle(repoBundlePath);  
            }    
            
            logger.info("Message resource deployed: " + name);
        }
        catch (Throwable e)
        {
            throw new AlfrescoRuntimeException("Message resource deployment failed", e);
        }      
    }   
    
    public void undeployMessageBundle(String bundleBaseName)
    {   
        checkBundleBaseName(bundleBaseName);
    
        try
        {
            StoreRef storeRef = repoMessagesLocation.getStoreRef();
            
            // unregister bundle
            String repoBundlePath = storeRef.toString() + repoMessagesLocation.getPath() + "/cm:" + bundleBaseName;
            messageService.unregisterResourceBundle(repoBundlePath);
              
            NodeRef rootNode = nodeService.getRootNode(storeRef);
             
            List nodeRefs = searchService.selectNodes(rootNode, repoMessagesLocation.getPath()+CRITERIA_ALL, null, namespaceService, false);
                    
            boolean found = false;
            for (NodeRef nodeRef : nodeRefs)
            {
                String resourceName = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_NAME);
                
                if (bundleBaseName.equals(messageService.getBaseBundleName(resourceName)))
                {
                    // remove message resource file from the repository
                    nodeService.deleteNode(nodeRef);
                    found = true; // continue to undeploy any others
                }               
            }           
            if (found)
            {          
                logger.info("Message resources undeployed: " + bundleBaseName);
            }
            else
            {
                throw new AlfrescoRuntimeException(MSG_RESOURCES_NOT_FOUND, new Object[] { repoBundlePath });
            }
        }
        catch (Throwable t)
        {
            throw new AlfrescoRuntimeException(MSG_RESOURCES_UNDEPLOYMENT_FAILED, t);
        }
    }
    
    public void reloadMessageBundle(String bundleBaseName)
    {
        checkBundleBaseName(bundleBaseName);
    
        try
        {
            StoreRef storeRef = repoMessagesLocation.getStoreRef();
            
            // re-register bundle
            
            String repoBundlePath = storeRef.toString() + repoMessagesLocation.getPath() + "/cm:" + bundleBaseName;
             
            NodeRef rootNode = nodeService.getRootNode(storeRef);
             
            List nodeRefs = searchService.selectNodes(rootNode, repoMessagesLocation.getPath()+CRITERIA_ALL, null, namespaceService, false);
                    
            boolean found = false;
            for (NodeRef nodeRef : nodeRefs)
            {
                String resourceName = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_NAME);
                
                if (bundleBaseName.equals(messageService.getBaseBundleName(resourceName)))
                {
                    found = true;
                    break;
                }               
            }
            
            if (found)
            {          
                messageService.unregisterResourceBundle(repoBundlePath);           
                messageService.registerResourceBundle(repoBundlePath);
    
                logger.info("Message resources re-loaded: " + bundleBaseName);
            }
            else
            {
                throw new AlfrescoRuntimeException(MSG_RESOURCES_NOT_FOUND, new Object[] { repoBundlePath });
            }
        }
        catch (Throwable e)
        {
            throw new AlfrescoRuntimeException(MSG_RESOURCES_RELOAD_FAILED, e);
        }      
    }
    
    private void checkBundleBaseName(String bundleBaseName)
    {
        if ((bundleBaseName == null) || (bundleBaseName.equals("")))
        {
            throw new AlfrescoRuntimeException(MSG_MISSING_BUNDLE_BASE_NAME);
        }
        
        if (bundleBaseName.indexOf("_") != -1)
        {
            // currently limited due to parser in MessageServiceImpl.getBaseBundleName
            throw new AlfrescoRuntimeException(MSG_BASE_NAME_CONTAIN_UNDERSCORE, new Object[] { bundleBaseName });
        }
        
        if (bundleBaseName.indexOf(".") != -1)
        {
            throw new AlfrescoRuntimeException(MSG_BASE_NAME_CONTAIN_PERIOD, new Object[] { bundleBaseName });
        }
    }
    @Override
    public RepoUsage getRestrictions()
    {
        return repoUsageComponent.getRestrictions();
    }
    @Override
    public RepoUsage getUsage()
    {
        return repoUsageComponent.getUsage();
    }
    @Override
    public boolean updateUsage(UsageType usageType)
    {
        return repoUsageComponent.updateUsage(usageType);
    }
    @Override
    public RepoUsageStatus getUsageStatus()
    {
        return repoUsageComponent.getUsageStatus();
    }
    private String getLocalisedMessage(String msgId, Object... params)
    {
        String localisedMsg = I18NUtil.getMessage(msgId, params);
        if (localisedMsg == null)
        {
            localisedMsg = msgId;
        }
        return localisedMsg;
    }
}