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

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.importer.view.NodeContext;
import org.alfresco.repo.model.filefolder.HiddenAspect;
import org.alfresco.repo.policy.BehaviourFilter;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.usage.ContentUsageImpl;
import org.alfresco.repo.version.Version2Model;
import org.alfresco.repo.version.common.VersionUtil;
import org.alfresco.service.cmr.dictionary.AssociationDefinition;
import org.alfresco.service.cmr.dictionary.ChildAssociationDefinition;
import org.alfresco.service.cmr.dictionary.ClassDefinition;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.InvalidClassException;
import org.alfresco.service.cmr.dictionary.TypeDefinition;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
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.XPathException;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.cmr.rule.RuleService;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.security.AccessPermission;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.OwnableService;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.cmr.version.Version;
import org.alfresco.service.cmr.version.VersionService;
import org.alfresco.service.cmr.view.ImportPackageHandler;
import org.alfresco.service.cmr.view.ImporterBinding;
import org.alfresco.service.cmr.view.ImporterBinding.UUID_BINDING;
import org.alfresco.service.cmr.view.ImporterContentCache;
import org.alfresco.service.cmr.view.ImporterException;
import org.alfresco.service.cmr.view.ImporterProgress;
import org.alfresco.service.cmr.view.ImporterService;
import org.alfresco.service.cmr.view.Location;
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.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.extensions.surf.util.ParameterCheck;
import org.springframework.util.StringUtils;
import org.xml.sax.ContentHandler;


/**
 * Default implementation of the Importer Service
 *  
 * @author David Caruana
 */
public class ImporterComponent implements ImporterService
{
    // Logger
    private static final Log logger = LogFactory.getLog(ImporterComponent.class);
    
    // default importer
    // TODO: Allow registration of plug-in parsers (by namespace)
    private Parser viewParser;

    // supporting services
    private NamespaceService namespaceService;
    private DictionaryService dictionaryService;
    private BehaviourFilter behaviourFilter;
    private NodeService nodeService;
    private SearchService searchService;
    private ContentService contentService;
    private RuleService ruleService;
    private PermissionService permissionService;
    private AuthorityService authorityService;
    private OwnableService ownableService;
    private VersionService versionService;
    private HiddenAspect hiddenAspect;
    private ContentUsageImpl contentUsageImpl;

    /**
     * The db node service, used when updating the version store.
     */
    protected NodeService dbNodeService;


    // binding markers    
    private static final String START_BINDING_MARKER = "${";
    private static final String END_BINDING_MARKER = "}"; 
    
    
    /**
     * @param viewParser  the default parser
     */
    public void setViewParser(Parser viewParser)
    {
        this.viewParser = viewParser;
    }
    
    /**
     * @param nodeService  the node service
     */
    public void setNodeService(NodeService nodeService)
    {
        this.nodeService = nodeService;
    }

    /**
     * @param searchService the service to perform path searches
     */
    public void setSearchService(SearchService searchService)
    {
        this.searchService = searchService;
    }

    /**
     * @param contentService  the content service
     */
    public void setContentService(ContentService contentService)
    {
        this.contentService = contentService;
    }
    
    /**
     * @param dictionaryService  the dictionary service
     */
    public void setDictionaryService(DictionaryService dictionaryService)
    {
        this.dictionaryService = dictionaryService;
    }
    
    /**
     * @param namespaceService  the namespace service
     */
    public void setNamespaceService(NamespaceService namespaceService)
    {
        this.namespaceService = namespaceService;
    }

    /**
     * @param behaviourFilter  policy behaviour filter 
     */
    public void setBehaviourFilter(BehaviourFilter behaviourFilter)
    {
        this.behaviourFilter = behaviourFilter;
    }

    /**
     * TODO: Remove this in favour of appropriate rule disabling
     * 
     * @param ruleService  rule service
     */
    public void setRuleService(RuleService ruleService)
    {
        this.ruleService = ruleService;
    }
    
    /**
     * @param permissionService  permissionService
     */
    public void setPermissionService(PermissionService permissionService)
    {
        this.permissionService = permissionService;
    }
    
    /**
     * @param authorityService  authorityService
     */
    public void setAuthorityService(AuthorityService authorityService)
    {
        this.authorityService = authorityService;
    }
    
    /**
     * @param ownableService  ownableService
     */
    public void setOwnableService(OwnableService ownableService)
    {
        this.ownableService = ownableService;
    }

    /**
     * @param versionService  versionService
     */
    public void setVersionService(VersionService versionService)
    {
        this.versionService = versionService;
    }
    
    /**
     * Sets the db node service, used when updating the 
     *  versioning information
     *
     * @param nodeService  the node service
     */
    public void setDbNodeService(NodeService nodeService)
    {
        this.dbNodeService = nodeService;
    }
    
    public void setHiddenAspect(HiddenAspect hiddenAspect)
    {
        this.hiddenAspect = hiddenAspect;
    }

    /**
     * When all behaviour is disabled it is necessary to use {@link ContentUsageImpl}
     * directly to update user usage. An instance of this class (with ID
     * <code>importerComponentWithBehaviour</code>) that does not disable
     * behaviours is also defined in the system - in that case it should not reference
     * the {@link ContentUsageImpl} collaborator.
     * 
     * @param contentUsageImpl the contentUsageImpl to set
     */
    public void setContentUsageImpl(ContentUsageImpl contentUsageImpl)
    {
        this.contentUsageImpl = contentUsageImpl;
    }

    /* (non-Javadoc)
     * @see org.alfresco.service.cmr.view.ImporterService#importView(java.io.InputStreamReader, org.alfresco.service.cmr.view.Location, java.util.Properties, org.alfresco.service.cmr.view.ImporterProgress)
     */
    public void importView(Reader viewReader, Location location, ImporterBinding binding, ImporterProgress progress)
    {
        NodeRef nodeRef = getNodeRef(location, binding);
        parserImport(nodeRef, location, viewReader, new DefaultStreamHandler(), binding, progress);
    }

    /* (non-Javadoc)
     * @see org.alfresco.service.cmr.view.ImporterService#importView(org.alfresco.service.cmr.view.ImportPackageHandler, org.alfresco.service.cmr.view.Location, org.alfresco.service.cmr.view.ImporterBinding, org.alfresco.service.cmr.view.ImporterProgress)
     */
    public void importView(ImportPackageHandler importHandler, Location location, ImporterBinding binding, ImporterProgress progress) throws ImporterException
    {
        importHandler.startImport();
        Reader dataFileReader = importHandler.getDataStream(); 
        NodeRef nodeRef = getNodeRef(location, binding);
        parserImport(nodeRef, location, dataFileReader, importHandler, binding, progress);
        importHandler.endImport();
    }
    
    /**
     * Get Node Reference from Location
     *  
     * @param location the location to extract node reference from
     * @param binding import configuration
     * @return node reference
     */
    private NodeRef getNodeRef(Location location, ImporterBinding binding)
    {
        ParameterCheck.mandatory("Location", location);
    
        // Establish node to import within
        NodeRef nodeRef = location.getNodeRef();
        if (nodeRef == null)
        {
            // If a specific node has not been provided, default to the root
            nodeRef = nodeService.getRootNode(location.getStoreRef());
        }
        
        // Resolve to path within node, if one specified
        String path = location.getPath();
        if (path != null && path.length() >0)
        {
            // Create a valid path and search
            path = bindPlaceHolder(path, binding);
            path = createValidPath(path);
            List<NodeRef> nodeRefs = searchService.selectNodes(nodeRef, path, null, namespaceService, false);
            if (nodeRefs.size() == 0)
            {
                throw new ImporterException("Path " + path + " within node " + nodeRef + " does not exist - the path must resolve to a valid location");
            }
            if (nodeRefs.size() > 1)
            {
                throw new ImporterException("Path " + path + " within node " + nodeRef + " found too many locations - the path must resolve to one location");
            }
            nodeRef = nodeRefs.get(0);
        }
    
        // TODO: Check Node actually exists
        
        return nodeRef;
    }
    
    /**
     * Bind the specified value to the passed configuration values if it is a place holder
     * 
     * @param value  the value to bind
     * @param binding  the configuration properties to bind to
     * @return  the bound value
     */
    private String bindPlaceHolder(String value, ImporterBinding binding)
    {
        if (binding != null)
        {
            int iStartBinding = value.indexOf(START_BINDING_MARKER);
            while (iStartBinding != -1)
            {
                int iEndBinding = value.indexOf(END_BINDING_MARKER, iStartBinding + START_BINDING_MARKER.length());
                if (iEndBinding == -1)
                {
                    throw new ImporterException("Cannot find end marker " + END_BINDING_MARKER + " within value " + value);
                }
                
                String key = value.substring(iStartBinding + START_BINDING_MARKER.length(), iEndBinding);
                String keyValue = binding.getValue(key);
                if (keyValue == null) {
                    logger.warn("No binding value for placeholder (will default to empty string): " + value);
                }
                value = StringUtils.replace(value, START_BINDING_MARKER + key + END_BINDING_MARKER, keyValue == null ? "" : keyValue);
                iStartBinding = value.indexOf(START_BINDING_MARKER);
            }
        }
        return value;
    }
    
    /**
     * Create a valid qname-based xpath
     * 
     * Note: 
     * - the localname will be truncated to 100 chars
     * - the localname should already be encoded for ISO 9075 (in case of MT bootstrap, the @ sign will be auto-encoded, see below)
     * 
     * Some examples:
     *      /
     *      sys:people/cm:admin
     *      /app:company_home/app:dictionary
     *      ../../cm:people_x0020_folder
     *      sys:people/cm:admin_x0040_test
     *      
     * @param path String
     * @return String
     */
    private String createValidPath(String path)
    {
        StringBuffer validPath = new StringBuffer(path.length());
        String[] segments = StringUtils.delimitedListToStringArray(path, "/");
        for (int i = 0; i < segments.length; i++)
        {
            if (segments[i] != null && segments[i].length() > 0)
            {
                int colonIndex = segments[i].indexOf(QName.NAMESPACE_PREFIX);
                if (colonIndex == -1)
                {
                    // eg. ".."
                    validPath.append(segments[i]);
                }
                else
                {
                    String[] qnameComponents = QName.splitPrefixedQName(segments[i]);
                    
                    String localName = QName.createValidLocalName(qnameComponents[1]);
                    
                    // MT: bootstrap of "alfrescoUserStore.xml" requires 'sys:people/cm:admin@tenant' to be encoded as 'sys:people/cm:admin_x0040_tenant' (for XPath)
                    localName = localName.replace("@", "_x0040_");
                    
                    QName segmentQName = QName.createQName(qnameComponents[0], localName, namespaceService);
                    validPath.append(segmentQName.toPrefixString());
                }
            }
            if (i < (segments.length -1))
            {
                validPath.append("/");
            }
        }
        return validPath.toString();
    }
    
    /**
     * Perform Import via Parser
     * 
     * @param nodeRef node reference to import under
     * @param location the location to import under
     * @param viewReader the view Reader
     * @param streamHandler the content property import stream handler
     * @param binding import configuration
     * @param progress import progress
     */
    public void parserImport(NodeRef nodeRef, Location location, Reader viewReader, ImportPackageHandler streamHandler, ImporterBinding binding, ImporterProgress progress)
    {
        ParameterCheck.mandatory("Node Reference", nodeRef);
        ParameterCheck.mandatory("View Reader", viewReader);
        ParameterCheck.mandatory("Stream Handler", streamHandler);
        
        Importer nodeImporter = new NodeImporter(nodeRef, location, binding, streamHandler, progress);
        try
        {
            nodeImporter.start();
            viewParser.parse(viewReader, nodeImporter);
            nodeImporter.end();
        }
        catch(RuntimeException e)
        {
            nodeImporter.error(e);
            throw e;
        }
    }
    
    /**
     * Perform import via Content Handler
     * 
     * @param nodeRef node reference to import under
     * @param location the location to import under
     * @param handler the import content handler
     * @param binding import configuration
     * @param progress import progress
     * @return  content handler to interact with
     */
    public ContentHandler handlerImport(NodeRef nodeRef, Location location, ImportContentHandler handler, ImporterBinding binding, ImporterProgress progress)
    {
        ParameterCheck.mandatory("Node Reference", nodeRef);

        DefaultContentHandler defaultHandler = new DefaultContentHandler(handler);
        ImportPackageHandler streamHandler = new ContentHandlerStreamHandler(defaultHandler);
        Importer nodeImporter = new NodeImporter(nodeRef, location, binding, streamHandler, progress);
        defaultHandler.setImporter(nodeImporter);
        return defaultHandler;        
    }

    /**
     * Encapsulate how a node is imported into the repository
     */
    public interface NodeImporterStrategy
    {
        /**
         * Import a node
         * 
         * @param  node to import
         */
        public NodeRef importNode(ImportNode node);
    }
    
    /**
     * Default Importer strategy
     * 
     * @author David Caruana
     */
    private class NodeImporter
        implements Importer
    {
        private NodeRef rootRef;
        private QName rootAssocType;
        private Location location;
        private ImporterBinding binding;
        private ImporterProgress progress;
        private ImportPackageHandler streamHandler;
        private NodeImporterStrategy importStrategy;
        private UpdateExistingNodeImporterStrategy updateStrategy;
        private QName[] excludedClasses;

        // Import tracking
        private List<ImportedNodeRef> nodeRefs = new ArrayList<ImportedNodeRef>();

        /**
         * Construct
         * 
         * @param rootRef NodeRef
         * @param location Location
         * @param binding ImporterBinding
         * @param streamHandler ImportPackageHandler
         * @param progress ImporterProgress
         */
        private NodeImporter(NodeRef rootRef, Location location, ImporterBinding binding, ImportPackageHandler streamHandler, ImporterProgress progress)
        {
            this.rootRef = rootRef;
            this.rootAssocType = location.getChildAssocType();
            this.location = location;
            this.binding = binding;
            this.progress = progress;
            this.streamHandler = streamHandler;
            this.importStrategy = createNodeImporterStrategy(binding == null ? null : binding.getUUIDBinding());
            this.updateStrategy = new UpdateExistingNodeImporterStrategy();

            // initialise list of content models to exclude from import
            if (binding == null || binding.getExcludedClasses() == null)
            {
                this.excludedClasses = new QName[] { ContentModel.ASPECT_REFERENCEABLE };
            }
            else
            {
                this.excludedClasses = binding.getExcludedClasses();
            }
        }

        /**
         * Create Node Importer Strategy
         * 
         * @param uuidBinding  UUID Binding
         * @return  Node Importer Strategy
         */
        private NodeImporterStrategy createNodeImporterStrategy(ImporterBinding.UUID_BINDING uuidBinding)
        {
            if (uuidBinding == null)
            {
                return new CreateNewNodeImporterStrategy(true);
            }
            else if (uuidBinding.equals(UUID_BINDING.CREATE_NEW))
            {
                return new CreateNewNodeImporterStrategy(true);
            }
            else if (uuidBinding.equals(UUID_BINDING.CREATE_NEW_WITH_UUID))
            {
                return new CreateNewNodeImporterStrategy(false);
            }
            else if (uuidBinding.equals(UUID_BINDING.REMOVE_EXISTING))
            {
                return new RemoveExistingNodeImporterStrategy();
            }
            else if (uuidBinding.equals(UUID_BINDING.REPLACE_EXISTING))
            {
                return new ReplaceExistingNodeImporterStrategy();
            }
            else if (uuidBinding.equals(UUID_BINDING.UPDATE_EXISTING))
            {
                return new UpdateExistingNodeImporterStrategy();
            }
            else if (uuidBinding.equals(UUID_BINDING.THROW_ON_COLLISION))
            {
                return new ThrowOnCollisionNodeImporterStrategy();
            }
            else
            {
                return new CreateNewNodeImporterStrategy(true);
            }
        }
        
        /* (non-Javadoc)
         * @see org.alfresco.repo.importer.Importer#getRootRef()
         */
        public NodeRef getRootRef()
        {
            return rootRef;
        }

        /* (non-Javadoc)
         * @see org.alfresco.repo.importer.Importer#getRootAssocType()
         */
        public QName getRootAssocType()
        {
            return rootAssocType;
        }

        @Override
        public Location getLocation()
        {
            return location;
        }

        /* (non-Javadoc)
         * @see org.alfresco.repo.importer.Importer#start()
         */
        public void start()
        {
            reportStarted();
        }
       
        /* (non-Javadoc)
         * @see org.alfresco.repo.importer.Importer#importMetaData(java.util.Map)
         */
        public void importMetaData(Map<QName, String> properties)
        {
            // Determine if we're importing a complete repository
            String complexPath = properties.get(QName.createQName(NamespaceService.REPOSITORY_VIEW_1_0_URI, "exportOf"));
            for (String path : complexPath.split(","))
            {
                if (path != null && path.equals("/"))
                {
                    // Only allow complete repository import into root
                    NodeRef storeRootRef = nodeService.getRootNode(rootRef.getStoreRef());
                    if (!storeRootRef.equals(rootRef))
                    {
                        throw new ImporterException("A complete repository package cannot be imported here");
                    }
                }
            }
        }

        /* (non-Javadoc)
         * @see org.alfresco.repo.importer.Importer#importNode(org.alfresco.repo.importer.ImportNode)
         */
        @SuppressWarnings("unchecked")
        public NodeRef importNode(ImportNode context)
        {
            // import node
            NodeRef nodeRef;
            if (context.isReference())
            {
                nodeRef = linkNode(context);
            }
            else
            {
                nodeRef = importStrategy.importNode(context);
            }
            
            // apply aspects
            for (QName aspect : context.getNodeAspects())
            {
                if (nodeService.hasAspect(nodeRef, aspect) == false)
                {
                    nodeService.addAspect(nodeRef, aspect, null);   // all properties previously added
                    reportAspectAdded(nodeRef, aspect);
                }
            }

            // check whether the node should be hidden
            hiddenAspect.checkHidden(nodeRef, false, false);

            // import content, if applicable
            for (Map.Entry<QName,Serializable> property : context.getProperties().entrySet())
            {
                // filter out content properties (they're imported later)
                DataTypeDefinition valueDataType = context.getPropertyDataType(property.getKey());
                if (valueDataType != null && valueDataType.getName().equals(DataTypeDefinition.CONTENT))
                {
                    // the property may be a single value or a collection - handle both
                    Object objVal = property.getValue();
                    if (objVal instanceof String)
                    {
                       importContent(nodeRef, property.getKey(), (String)objVal);
                    }
                    else if (objVal instanceof Collection)
                    {
                       for (String value : (Collection<String>)objVal)
                       {
                          importContent(nodeRef, property.getKey(), value);
                       }
                    }
                }
            }
            
            // if the node has the versionable aspect applied to it,
            //  create an initial version for it
            if(context.getNodeAspects().contains(ContentModel.ASPECT_VERSIONABLE))
            {
                generateVersioningForVersionableNode(nodeRef);
            }
            
            return nodeRef;
        }
        
        /**
         * Fixes things up for versionable nodes after importing.
         * Because version information is stored in a different store,
         *  the past versions are not included in the ACP. 
         * However, because the node has the versionable aspect applied to 
         *  it, we still need it to have a single version in the version store.
         * This method arranges for that. 
         */
        private void generateVersioningForVersionableNode(final NodeRef nodeRef)
        {
            // Is versioning already turned on?
            if(versionService.getVersionHistory(nodeRef) != null)
            {
                // There is already version history, so we don't need to do anything
                return;
            }
            
            // Take a copy of the version label, as it'll be reset when
            //  we request that versioning occurs
            final String label = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_VERSION_LABEL);
            
            // Have versioning enabled
            Version version = versionService.createVersion(nodeRef, null);
            final NodeRef versionNodeRef = VersionUtil.convertNodeRef(version.getFrozenStateNodeRef());
            
            // Put the version label back how it should be on the main node
            dbNodeService.setProperty(nodeRef, ContentModel.PROP_VERSION_LABEL, label);
            
            // Fix up the versioned version node to be what it should be
            // (The previous version label should be off, and the current label is the new one)
            dbNodeService.setProperty(versionNodeRef, ContentModel.PROP_VERSION_LABEL, null);
            dbNodeService.setProperty(versionNodeRef, Version2Model.PROP_QNAME_VERSION_LABEL, label);
        }

        /**
         * Link an existing Node
         * 
         * @param context  node to link in
         * @return  node reference of child linked in
         */
        private NodeRef linkNode(ImportNode context)
        {
            ImportParent parentContext = context.getParentContext();
            NodeRef parentRef = parentContext.getParentRef();
            
            // determine the node reference to link to
            String uuid = context.getUUID();
            if (uuid == null || uuid.length() == 0)
            {
                throw new ImporterException("Node reference does not specify a reference to follow.");
            }
            NodeRef referencedRef = new NodeRef(rootRef.getStoreRef(), uuid);

            // Note: do not link references that are defined in the root of the import
            if (!parentRef.equals(getRootRef()))
            {
                // determine child assoc type
                QName assocType = getAssocType(context);
                AssociationDefinition assocDef = dictionaryService.getAssociation(assocType);
                if (assocDef.isChild())
                {
                    // determine child name
                    QName childQName = getChildName(context);
                    if (childQName == null)
                    {
                        String name = (String)nodeService.getProperty(referencedRef, ContentModel.PROP_NAME);
                        if (name == null || name.length() == 0)
                        {
                            throw new ImporterException("Cannot determine node reference child name");
                        }
                        String localName = QName.createValidLocalName(name);
                        childQName = QName.createQName(assocType.getNamespaceURI(), localName);
                    }
                
                    // create the secondary link
                    nodeService.addChild(parentRef, referencedRef, assocType, childQName);
                    reportNodeLinked(referencedRef, parentRef, assocType, childQName);
                }
                else
                {
                    nodeService.createAssociation(parentRef, referencedRef, assocType);
                    reportNodeLinked(parentRef, referencedRef, assocType, null);
                }
            }
            
            // second, perform any specified udpates to the node
            updateStrategy.importNode(context);
            return referencedRef; 
        }
        
        /**
         * Import Node Content.
         * <p>
         * The content URL, if present, will be a local URL.  This import copies the content
         * from the local URL to a server-assigned location.
         *
         * @param nodeRef containing node
         * @param propertyName the name of the content-type property
         * @param importContentData the identifier of the content to import
         */
        private void importContent(NodeRef nodeRef, QName propertyName, String importContentData)
        {
            ImporterContentCache contentCache = (binding == null) ? null : binding.getImportConentCache();
            
            // bind import content data description
            importContentData = bindPlaceHolder(importContentData, binding);
            if (importContentData != null && importContentData.length() > 0)
            {
                DataTypeDefinition dataTypeDef = dictionaryService.getDataType(DataTypeDefinition.CONTENT);
                ContentData contentData = (ContentData)DefaultTypeConverter.INSTANCE.convert(dataTypeDef, importContentData);
                String contentUrl = contentData.getContentUrl();
                if (contentUrl != null && contentUrl.length() > 0)
                {
                    Map<QName, Serializable> propsBefore = null;
                    if (contentUsageImpl != null && contentUsageImpl.getEnabled())
                    {
                        propsBefore = nodeService.getProperties(nodeRef);
                    }

                    if (contentCache != null)
                    {
                        // import content from source
                        ContentData cachedContentData = contentCache.getContent(streamHandler, contentData);
                        nodeService.setProperty(nodeRef, propertyName, cachedContentData);
                    }
                    else
                    {
                        // import the content from the import source file
                        InputStream contentStream = streamHandler.importStream(contentUrl);
                        ContentWriter writer = contentService.getWriter(nodeRef, propertyName, true);
                        writer.setEncoding(contentData.getEncoding());
                        writer.setMimetype(contentData.getMimetype());
                        writer.putContent(contentStream);
                    }
                                        
                    if (contentUsageImpl != null && contentUsageImpl.getEnabled())
                    {
                        // Since behaviours for content nodes have all been disabled,
                        // it is necessary to update the user's usage stats.
                        Map<QName, Serializable> propsAfter = nodeService.getProperties(nodeRef);
                        contentUsageImpl.onUpdateProperties(nodeRef, propsBefore, propsAfter);
                    }
                    
                    reportContentCreated(nodeRef, contentUrl);
                }
            }
        }
        
        /* (non-Javadoc)
         * @see org.alfresco.repo.importer.Importer#childrenImported(org.alfresco.service.cmr.repository.NodeRef)
         */
        public void childrenImported(NodeRef nodeRef)
        {
            behaviourFilter.enableBehaviour(nodeRef);
            ruleService.enableRules(nodeRef);
        }

        /* (non-Javadoc)
         * @see org.alfresco.repo.importer.Importer#resolvePath(java.lang.String)
         */
        public NodeRef resolvePath(String path)
        {
            NodeRef referencedRef = null;
            if (path != null && path.length() > 0)
            {
                referencedRef = resolveImportedNodeRef(rootRef, path);
            }
            return referencedRef;
        }

        /*
         *  (non-Javadoc)
         * @see org.alfresco.repo.importer.Importer#isExcludedClass(org.alfresco.service.namespace.QName)
         */
        public boolean isExcludedClass(QName className)
        {
            for (QName excludedClass : excludedClasses)
            {
                if (excludedClass.equals(className))
                {
                    return true;
                }
            }
            return false;
        }
        
        /* (non-Javadoc)
         * @see org.alfresco.repo.importer.Importer#end()
         */
        @SuppressWarnings("unchecked")
        public void end()
        {
            // Bind all node references to destination space
            for (ImportedNodeRef importedRef : nodeRefs)
            {
                Serializable refProperty = null;
                if (importedRef.value != null)
                {
                    if (importedRef.value instanceof Collection)
                    {
                        Collection<String> unresolvedRefs = (Collection<String>)importedRef.value;
                        List<NodeRef> resolvedRefs = new ArrayList<NodeRef>(unresolvedRefs.size());
                        for (String unresolvedRef : unresolvedRefs)
                        {
                            if (unresolvedRef != null)
                            {
                                NodeRef nodeRef = resolveImportedNodeRef(importedRef.context.getNodeRef(), unresolvedRef);
                                // TODO: Provide a better mechanism for invalid references? e.g. report warning
                                if (nodeRef != null)
                                {
                                    resolvedRefs.add(nodeRef);
                                }
                            }
                        }
                        refProperty = (Serializable)resolvedRefs;
                    }
                    else
                    {
                        refProperty = resolveImportedNodeRef(importedRef.context.getNodeRef(), (String)importedRef.value);
                        // TODO: Provide a better mechanism for invalid references? e.g. report warning
                    }
                }
                
                // Set node reference on source node
                Set<QName> nodeTypeAndAspects = getNodeTypeAndAspects(importedRef.context);
                try
                {
                    for (QName typeOrAspect: nodeTypeAndAspects)
                    {
                        behaviourFilter.disableBehaviour(importedRef.context.getNodeRef(), typeOrAspect);
                    }
                    nodeService.setProperty(importedRef.context.getNodeRef(), importedRef.property, refProperty);
                    if (progress != null)
                    {
                        progress.propertySet(importedRef.context.getNodeRef(), importedRef.property, refProperty);
                    }
                }
                finally
                {
                    for (QName typeOrAspect: nodeTypeAndAspects)
                    {
                        behaviourFilter.enableBehaviour(importedRef.context.getNodeRef(), typeOrAspect);
                    }
                }
            }
            
            reportCompleted();
        }

        /*
         *  (non-Javadoc)
         * @see org.alfresco.repo.importer.Importer#error(java.lang.Throwable)
         */
        public void error(Throwable e)
        {
            behaviourFilter.enableBehaviour();
            reportError(e);
        }

        /**
         * Get the child name to import node under
         * 
         * @param context  the node
         * @return  the child name
         */
        private QName getChildName(ImportNode context)
        {
            QName assocType = getAssocType(context);
            QName childQName = null;
    
            // Determine child name
            String childName = context.getChildName();
            if (childName != null)
            {
                childName = bindPlaceHolder(childName, binding);
                // <Fix for ETHREEOH-2299>
                if (ContentModel.TYPE_PERSON.equals(context.getTypeDefinition().getName())
                        && assocType.equals(ContentModel.ASSOC_CHILDREN))
                {
                    childName = childName.toLowerCase();
                }
                // </Fix for ETHREEOH-2299>
                String[] qnameComponents = QName.splitPrefixedQName(childName);
                childQName = QName.createQName(qnameComponents[0], QName.createValidLocalName(qnameComponents[1]), namespaceService); 
            }
            else
            {
                Map<QName, Serializable> typeProperties = context.getProperties();
                
                Serializable nameValue = typeProperties.get(ContentModel.PROP_NAME);

                if(nameValue != null && !String.class.isAssignableFrom(nameValue.getClass()))
                {
                    throw new  ImporterException("Unable to use childName property: "+ ContentModel.PROP_NAME + " is not a string");  
                }
                
                String name = (String)nameValue;
                
                if (name != null && name.length() > 0)
                {
                    name = bindPlaceHolder(name, binding);
                    String localName = QName.createValidLocalName(name);
                    childQName = QName.createQName(assocType.getNamespaceURI(), localName);
                }
            }
            
            return childQName;
        }
        
        /**
         * Get appropriate child association type for node to import under
         * 
         * @param context  node to import
         * @return  child association type name
         */
        private QName getAssocType(ImportNode context)
        {
            QName assocType = context.getParentContext().getAssocType();
            if (assocType != null)
            {
                // return explicitly set association type
                return assocType;
            }
            
            //
            // Derive association type
            //
            
            // build type and aspect list for node
            List<QName> nodeTypes = new ArrayList<QName>();
            nodeTypes.add(context.getTypeDefinition().getName());
            for (QName aspect : context.getNodeAspects())
            {
                nodeTypes.add(aspect);
            }
            
            // build target class types for parent
            Map<QName, QName> targetTypes = new HashMap<QName, QName>();
            QName parentType = nodeService.getType(context.getParentContext().getParentRef());
            ClassDefinition classDef = dictionaryService.getClass(parentType);
            Map<QName, ChildAssociationDefinition> childAssocDefs = classDef.getChildAssociations();
            for (ChildAssociationDefinition childAssocDef : childAssocDefs.values())
            {
                targetTypes.put(childAssocDef.getTargetClass().getName(), childAssocDef.getName());
            }
            Set<QName> parentAspects = nodeService.getAspects(context.getParentContext().getParentRef());
            for (QName parentAspect : parentAspects)
            {
                classDef = dictionaryService.getClass(parentAspect);
                if (classDef == null)
                {
                    throw new InvalidClassException(
                            "Failed import for context '" + context.getParentContext() + "'.  Unknown aspect: " + parentAspect,
                            parentAspect);
                }
                childAssocDefs = classDef.getChildAssociations();
                for (ChildAssociationDefinition childAssocDef : childAssocDefs.values())
                {
                    targetTypes.put(childAssocDef.getTargetClass().getName(), childAssocDef.getName());
                }
            }
            
            // find target class that is closest to node type or aspects
            QName closestAssocType = null;
            int closestHit = 1;
            for (QName nodeType : nodeTypes)
            {
                for (QName targetType : targetTypes.keySet())
                {
                    QName testType = nodeType;
                    int howClose = 1;
                    while (testType != null)
                    {
                        howClose--;
                        if (targetType.equals(testType) && howClose < closestHit)
                        {
                            closestAssocType = targetTypes.get(targetType);
                            closestHit = howClose;
                            break;
                        }
                        ClassDefinition testTypeDef = dictionaryService.getClass(testType);
                        testType = (testTypeDef == null) ? null : testTypeDef.getParentName();
                    }
                }
            }
            
            return closestAssocType;
        }
        
        /**
         * For the given import node, return the type and aspects to import
         * 
         * @param context  import node
         * @return  the type and aspects in the import
         */
        private Set<QName> getNodeTypeAndAspects(ImportNode context)
        {
            Set<QName> classNames = new HashSet<QName>();
            
            // disable the type
            TypeDefinition typeDef = context.getTypeDefinition();
            classNames.add(typeDef.getName());

            // disable the aspects imported on the node
            classNames.addAll(context.getNodeAspects());
            
            // note: do not disable default aspects that are not imported on the node.
            //       this means they'll be added on import
            
            return classNames;
        }
        
        /**
         * Bind properties
         * 
         * @param context ImportNode
         * @return Map
         */
        @SuppressWarnings("unchecked")
        private Map<QName, Serializable> bindProperties(ImportNode context)
        {
            Map<QName, Serializable> properties = context.getProperties();
            Map<QName, Serializable> boundProperties = new HashMap<QName, Serializable>(properties.size());
            for (QName property : properties.keySet())
            {
                // get property datatype
                DataTypeDefinition valueDataType = context.getPropertyDataType(property);

                // filter out content properties (they're imported later)
                if (valueDataType != null && valueDataType.getName().equals(DataTypeDefinition.CONTENT))
                {
                    continue;
                }

                // get property value
                Serializable value = properties.get(property);
                
                // bind property value to configuration and convert to appropriate type
                if (value instanceof Collection)
                {
                    List<Serializable> boundCollection = new ArrayList<Serializable>();
                    for (Serializable collectionValue : (Collection<Serializable>)value)
                    {
                        Serializable objValue = bindValue(context, property, valueDataType, collectionValue);
                        boundCollection.add(objValue);
                    }
                    value = (Serializable)boundCollection;
                }
                else
                {
                    value = bindValue(context, property, valueDataType, value);
                }

                // choose to provide property on node creation or at end of import for lazy binding
                if (valueDataType != null && (valueDataType.getName().equals(DataTypeDefinition.NODE_REF) || valueDataType.getName().equals(DataTypeDefinition.CATEGORY)))
                {
                    // record node reference for end-of-import binding
                    ImportedNodeRef importedRef = new ImportedNodeRef(context, property, value);
                    nodeRefs.add(importedRef);
                }
                else
                {
                    // property ready to be set on Node creation / update
                    boundProperties.put(property, value);
                }
            }
            
            return boundProperties;
        }
        
        /**
         * Bind permissions - binds authorities
         * 
         * @param permissions List<AccessPermission>
         * @return List<AccessPermission>
         */
        private List<AccessPermission> bindPermissions(List<AccessPermission> permissions)
        {
            List<AccessPermission> boundPermissions = new ArrayList<AccessPermission>(permissions.size());
            
            for (AccessPermission permission : permissions)
            {
                AccessPermission ace = new NodeContext.ACE(permission.getAccessStatus(),
                                                           bindPlaceHolder(permission.getAuthority(), binding),
                                                           permission.getPermission());
                boundPermissions.add(ace);
            }
            
            return boundPermissions;
        }

        /**
         * Bind property value
         * 
         * @param valueType  value type
         * @param value  string form of value
         * @return  the bound value
         */
        private Serializable bindValue(ImportNode context, QName property, DataTypeDefinition valueType, Serializable value)
        {
            Serializable objValue = null;
            if (value != null && valueType != null)
            {
                if (value instanceof String)
                {
                    value = bindPlaceHolder(value.toString(), binding);
                }
                if ((valueType.getName().equals(DataTypeDefinition.NODE_REF) || valueType.getName().equals(DataTypeDefinition.CATEGORY)))
                {
                    objValue = value;
                }
                else
                {
                    objValue = (Serializable) DefaultTypeConverter.INSTANCE.convert(valueType, value);
                }
                
            }
            return objValue;
        }

        /**
         * Resolve imported reference relative to specified node
         *  
         * @param sourceNodeRef  context to resolve within
         * @param importedRef  reference to resolve
         * @return NodeRef
         */
        private NodeRef resolveImportedNodeRef(NodeRef sourceNodeRef, String importedRef)
        {
            // Resolve path to node reference
            NodeRef nodeRef = null;
            importedRef = bindPlaceHolder(importedRef, binding);
            
            if (importedRef.equals("/"))
            {
                nodeRef = sourceNodeRef;
            }
            else if (importedRef.startsWith("/"))
            {
                String path = createValidPath(importedRef);
                List<NodeRef> nodeRefs = searchService.selectNodes(sourceNodeRef, path, null, namespaceService, false);
                if (nodeRefs.size() > 0)
                {
                    nodeRef = nodeRefs.get(0);
                }
            }
            else
            {
            	// determine if node reference
            	if (NodeRef.isNodeRef(importedRef))
            	{
            		nodeRef = new NodeRef(importedRef);
            	}
            	else
            	{
	                // resolve relative path
	                try
	                {
	                    String path = createValidPath(importedRef);
	                    List<NodeRef> nodeRefs = searchService.selectNodes(sourceNodeRef, path, null, namespaceService, false);
	                    if (nodeRefs.size() > 0)
	                    {
	                        nodeRef = nodeRefs.get(0);
	                    }
	                }
	                catch(XPathException e)
	                {
	                    nodeRef = new NodeRef(importedRef);
	                }
	                catch(AlfrescoRuntimeException e1)
	                {
	                    // Note: Invalid reference format - try path search instead
	                }
            	}
            }
            
            return nodeRef;
        }
        
        /**
         * Helper to report start of import
         */
        private void reportStarted()
        {
            if (progress != null)
            {
                progress.started();
            }
        }
        
        /**
         * Helper to report end of import
         */
        private void reportCompleted()
        {
            if (progress != null)
            {
                progress.completed();
            }
        }
        
        /**
         * Helper to report error
         * 
         * @param e Throwable
         */
        private void reportError(Throwable e)
        {
            if (progress != null)
            {
                progress.error(e);
            }
        }
        
        /**
         * Helper to report node created progress
         * 
         * @param childAssocRef ChildAssociationRef
         */
        private void reportNodeCreated(ChildAssociationRef childAssocRef)
        {
            if (progress != null)
            {
                progress.nodeCreated(childAssocRef.getChildRef(), childAssocRef.getParentRef(), childAssocRef.getTypeQName(), childAssocRef.getQName());
            }
        }

        /**
         * Helper to report node linked progress
         * 
         * @param childRef NodeRef
         * @param parentRef NodeRef
         * @param assocType QName
         * @param childName QName
         */
        private void reportNodeLinked(NodeRef childRef, NodeRef parentRef, QName assocType, QName childName)
        {
            if (progress != null)
            {
                progress.nodeLinked(childRef, parentRef, assocType, childName);
            }
        }

        /**
         * Helper to report content created progress
         * 
         * @param nodeRef NodeRef
         * @param sourceUrl String
         */
        private void reportContentCreated(NodeRef nodeRef, String sourceUrl)
        {
            if (progress != null)
            {
                progress.contentCreated(nodeRef, sourceUrl);
            }
        }
        
        /**
         * Helper to report aspect added progress
         *  
         * @param nodeRef NodeRef
         * @param aspect QName
         */
        private void reportAspectAdded(NodeRef nodeRef, QName aspect)
        {
            if (progress != null)
            {
                progress.aspectAdded(nodeRef, aspect);
            }        
        }

        /**
         * Helper to report property set progress
         * 
         * @param nodeRef NodeRef
         * @param properties Map<QName, Serializable>
         */
        private void reportPropertySet(NodeRef nodeRef, Map<QName, Serializable> properties)
        {
            if (progress != null && properties != null)
            {
                for (QName property : properties.keySet())
                {
                    progress.propertySet(nodeRef, property, properties.get(property));
                }
            }
        }

        /**
         * Helper to report permission set progress
         * 
         * @param nodeRef NodeRef
         * @param permissions List<AccessPermission>
         */
        private void reportPermissionSet(NodeRef nodeRef, List<AccessPermission> permissions)
        {
            if (progress != null && permissions != null)
            {
                for (AccessPermission permission : permissions)
                {
                    progress.permissionSet(nodeRef, permission);
                }
            }
        }
        
        /**
         * Import strategy where imported nodes are always created regardless of whether a
         * node of the same UUID already exists in the repository
         */
        private class CreateNewNodeImporterStrategy implements NodeImporterStrategy
        {
            // force allocation of new UUID, even if one already specified
            private boolean assignNewUUID;
            
            /**
             * Construct
             * 
             * @param assignNewUUID  force allocation of new UUID
             */
            public CreateNewNodeImporterStrategy(boolean assignNewUUID)
            {
                this.assignNewUUID = assignNewUUID;
            }
            
            /*
             *  (non-Javadoc)
             * @see org.alfresco.repo.importer.ImporterComponent.NodeImporterStrategy#importNode(org.alfresco.repo.importer.ImportNode)
             */
            public NodeRef importNode(ImportNode node)
            {
                TypeDefinition nodeType = node.getTypeDefinition();
                NodeRef parentRef = node.getParentContext().getParentRef();
                QName assocType = getAssocType(node);
                QName childQName = getChildName(node);
                if (childQName == null)
                {
                    throw new ImporterException("Cannot determine child name of node (type: " + nodeType.getName() + ")");
                }

                // Disable the import-wide behaviours (because the node doesn't exist, yet)
                Set<QName> nodeTypeAndAspects = getNodeTypeAndAspects(node);
                for (QName typeOrAspect: nodeTypeAndAspects)
                {
                    behaviourFilter.disableBehaviour(typeOrAspect);
                }
                
                // Build initial map of properties
                Map<QName, Serializable> initialProperties = bindProperties(node);
                
                // Assign UUID if already specified on imported node
                if (!assignNewUUID && node.getUUID() != null)
                {
                    initialProperties.put(ContentModel.PROP_NODE_UUID, node.getUUID());
                }
                
                // Create Node
                ChildAssociationRef assocRef = nodeService.createNode(parentRef, assocType, childQName, nodeType.getName(), initialProperties);
                NodeRef nodeRef = assocRef.getChildRef();

                // Note: non-admin authorities take ownership of new nodes
                // from Thor
                if (!(AuthenticationUtil.isRunAsUserTheSystemUser() || authorityService.hasAdminAuthority()))
                {
                    ownableService.takeOwnership(nodeRef);
                }

                // apply permissions
                List<AccessPermission> permissions = null;
                AccessStatus writePermission = permissionService.hasPermission(nodeRef, PermissionService.CHANGE_PERMISSIONS);

                // from Thor
                if (AuthenticationUtil.isRunAsUserTheSystemUser() || writePermission.equals(AccessStatus.ALLOWED))
                {
                    permissions = bindPermissions(node.getAccessControlEntries());
                    
                    for (AccessPermission permission : permissions)
                    {
                        permissionService.setPermission(nodeRef, permission.getAuthority(), permission.getPermission(), permission.getAccessStatus().equals(AccessStatus.ALLOWED));
                    }
                    // note: apply inheritance after setting permissions as this may affect whether you can apply permissions
                    boolean inheritPermissions = node.getInheritPermissions();
                    if (!inheritPermissions)
                    {
                        permissionService.setInheritParentPermissions(nodeRef, false);
                    }
                }
                
                // Re-enable the import-wide behaviours
                // Disable behaviour for the node until the complete node (and its children have been imported)
                for (QName typeOrAspect: nodeTypeAndAspects)
                {
                    behaviourFilter.enableBehaviour(typeOrAspect);
                }
                behaviourFilter.disableBehaviour(nodeRef);
                // TODO: Replace this with appropriate rule/action import handling
                ruleService.disableRules(nodeRef);

                // Report creation
                reportNodeCreated(assocRef);
                reportPropertySet(nodeRef, initialProperties);
                reportPermissionSet(nodeRef, permissions);

                // return newly created node reference
                return nodeRef;
            }
        }
        
        /**
         * Importer strategy where an existing node (one with the same UUID) as a node being
         * imported is first removed.  The imported node is placed in the location specified
         * at import time. 
         */
        private class RemoveExistingNodeImporterStrategy implements NodeImporterStrategy
        {
            private NodeImporterStrategy createNewStrategy = new CreateNewNodeImporterStrategy(false);
            
            /*
             *  (non-Javadoc)
             * @see org.alfresco.repo.importer.ImporterComponent.NodeImporterStrategy#importNode(org.alfresco.repo.importer.ImportNode)
             */
            public NodeRef importNode(ImportNode node)
            {                
                // remove existing node, if node to import has a UUID and an existing node of the same
                // uuid already exists
                String uuid = node.getUUID();
                if (uuid != null && uuid.length() > 0)
                {
                    NodeRef existingNodeRef = new NodeRef(rootRef.getStoreRef(), uuid);
                    if (nodeService.exists(existingNodeRef))
                    {
                        // remove primary parent link forcing deletion
                        ChildAssociationRef childAssocRef = nodeService.getPrimaryParent(existingNodeRef);
                        
                        // TODO: Check for root node
                        nodeService.removeChild(childAssocRef.getParentRef(), childAssocRef.getChildRef());
                    }
                }
                
                // import as if a new node into current import parent location
                return createNewStrategy.importNode(node);
            }
        }

        /**
         * Importer strategy where an existing node (one with the same UUID) as a node being
         * imported is first removed.  The imported node is placed under the parent of the removed
         * node.
         */        
        private class ReplaceExistingNodeImporterStrategy implements NodeImporterStrategy
        {
            private NodeImporterStrategy createNewStrategy = new CreateNewNodeImporterStrategy(false);

            /*
             *  (non-Javadoc)
             * @see org.alfresco.repo.importer.ImporterComponent.NodeImporterStrategy#importNode(org.alfresco.repo.importer.ImportNode)
             */
            public NodeRef importNode(ImportNode node)
            {                
                // replace existing node, if node to import has a UUID and an existing node of the same
                // uuid already exists
                String uuid = node.getUUID();
                if (uuid != null && uuid.length() > 0)
                {
                    NodeRef existingNodeRef = new NodeRef(rootRef.getStoreRef(), uuid);
                    if (nodeService.exists(existingNodeRef))
                    {
                        // remove primary parent link forcing deletion
                        ChildAssociationRef childAssocRef = nodeService.getPrimaryParent(existingNodeRef);
                        nodeService.removeChild(childAssocRef.getParentRef(), childAssocRef.getChildRef());
                        
                        // update the parent context of the node being imported to the parent of the node just deleted
                        node.getParentContext().setParentRef(childAssocRef.getParentRef());
                        node.getParentContext().setAssocType(childAssocRef.getTypeQName());
                    }
                }

                // import as if a new node
                return createNewStrategy.importNode(node);
            }
        }
        
        /**
         * Import strategy where an error is thrown when importing a node that has the same UUID
         * of an existing node in the repository.
         */
        private class ThrowOnCollisionNodeImporterStrategy implements NodeImporterStrategy
        {
            private NodeImporterStrategy createNewStrategy = new CreateNewNodeImporterStrategy(false);

            /*
             *  (non-Javadoc)
             * @see org.alfresco.repo.importer.ImporterComponent.NodeImporterStrategy#importNode(org.alfresco.repo.importer.ImportNode)
             */
            public NodeRef importNode(ImportNode node)
            {
                // if node to import has a UUID and an existing node of the same uuid already exists
                // then throw an error
                String uuid = node.getUUID();
                if (uuid != null && uuid.length() > 0)
                {
                    NodeRef existingNodeRef = new NodeRef(rootRef.getStoreRef(), uuid);
                    if (nodeService.exists(existingNodeRef))
                    {
                        throw new InvalidNodeRefException("Node " + existingNodeRef + " already exists", existingNodeRef);
                    }
                }
                
                // import as if a new node
                return createNewStrategy.importNode(node);
            }
        }
        
        /**
         * Import strategy where imported nodes are updated if a node with the same UUID
         * already exists in the repository.
         * 
         * Note: this will only allow incremental update of an existing node - it does not
         *       delete properties or associations.
         */
        private class UpdateExistingNodeImporterStrategy implements NodeImporterStrategy
        {
            private NodeImporterStrategy createNewStrategy = new CreateNewNodeImporterStrategy(false);
            
            /*
             *  (non-Javadoc)
             * @see org.alfresco.repo.importer.ImporterComponent.NodeImporterStrategy#importNode(org.alfresco.repo.importer.ImportNode)
             */
            public NodeRef importNode(ImportNode node)
            {
                // replace existing node, if node to import has a UUID and an existing node of the same
                // uuid already exists
                String uuid = node.getUUID();
                NodeRef existingNodeRef = null;
                if (uuid == null && location.getPath() != null ) //need a valid location where to search
                {
                    NodeRef parentNodeRef = node.getParentContext().getParentRef();

                    // Resolve to path within node, if one specified
                    String path = location.getPath() + "/" + QName.createQName(node.getTypeDefinition().getName().getNamespaceURI(), node.getChildName()).toPrefixString();
                    // Search the node by name
                    List<NodeRef> nodeRefs = searchService.selectNodes(parentNodeRef, path, null, namespaceService, false);
                    if (!nodeRefs.isEmpty())
                    {
                        existingNodeRef = nodeRefs.get(0);
                    }
                }
                if (uuid != null && uuid.length() > 0 || existingNodeRef != null)
                {
                    if (existingNodeRef == null)
                    {
                        existingNodeRef = new NodeRef(rootRef.getStoreRef(), uuid);
                    }
                    if (nodeService.exists(existingNodeRef))
                    {
                        // do the update
                        Map<QName, Serializable> existingProperties = nodeService.getProperties(existingNodeRef);
                        Map<QName, Serializable> updateProperties = bindProperties(node);
                        if (updateProperties != null && updateProperties.size() > 0)
                        {
                            existingProperties.putAll(updateProperties);
                            nodeService.setProperties(existingNodeRef, existingProperties);
                        }
                        
                        // Apply permissions
                        List<AccessPermission> permissions = null;
                        AccessStatus writePermission = permissionService.hasPermission(existingNodeRef, PermissionService.CHANGE_PERMISSIONS);
                        
                        // from Thor
                        if (AuthenticationUtil.isRunAsUserTheSystemUser() || writePermission.equals(AccessStatus.ALLOWED))
                        {
                            boolean inheritPermissions = node.getInheritPermissions();
                            if (!inheritPermissions)
                            {
                                permissionService.setInheritParentPermissions(existingNodeRef, false);
                            }
                            
                            permissions = bindPermissions(node.getAccessControlEntries());
                            
                            for (AccessPermission permission : permissions)
                            {
                                permissionService.setPermission(existingNodeRef, permission.getAuthority(), permission.getPermission(), permission.getAccessStatus().equals(AccessStatus.ALLOWED));
                            }
                        }
                        
                        if(logger.isDebugEnabled())
                        {
                            logger.debug("Updating existing node " + existingNodeRef + " at " +
                                    nodeService.getPath(existingNodeRef) + " for " + node.toString());
                        }

                        // report update
                        reportPropertySet(existingNodeRef, updateProperties);
                        reportPermissionSet(existingNodeRef, permissions);
                        
                        return existingNodeRef;
                    }
                }
                
                // import as if a new node
                return createNewStrategy.importNode(node);
            }
        }

    }

    /**
     * Imported Node Reference
     * 
     * @author David Caruana
     */
    private static class ImportedNodeRef
    {
        /**
         * Construct
         * 
         * @param context ImportNode
         * @param property QName
         * @param value Serializable
         */
        private ImportedNodeRef(ImportNode context, QName property, Serializable value)
        {
            this.context = context;
            this.property = property;
            this.value = value;
        }
        
        private ImportNode context;
        private QName property;
        private Serializable value;
    }

    /**
     * Default Import Stream Handler
     * 
     * @author David Caruana
     */
    private static class DefaultStreamHandler
        implements ImportPackageHandler
    {
        /* (non-Javadoc)
         * @see org.alfresco.service.cmr.view.ImportPackageHandler#startImport()
         */
        public void startImport()
        {
        }

        /* (non-Javadoc)
         * @see org.alfresco.service.cmr.view.ImportStreamHandler#importStream(java.lang.String)
         */
        public InputStream importStream(String content)
        {
            ResourceLoader loader = new DefaultResourceLoader();
            Resource resource = loader.getResource(content);
            if (resource.exists() == false)
            {
                throw new ImporterException("Content URL " + content + " does not exist.");
            }
            
            try
            {
                return resource.getInputStream();
            }
            catch(IOException e)
            {
                throw new ImporterException("Failed to retrieve input stream for content URL " + content);
            }
        }

        /* (non-Javadoc)
         * @see org.alfresco.service.cmr.view.ImportPackageHandler#getDataStream()
         */
        public Reader getDataStream()
        {
            return null;
        }

        /* (non-Javadoc)
         * @see org.alfresco.service.cmr.view.ImportPackageHandler#endImport()
         */
        public void endImport()
        {
        }
    }

    /**
     * Default Import Stream Handler
     * 
     * @author David Caruana
     */
    private static class ContentHandlerStreamHandler
        implements ImportPackageHandler
    {
        private ImportContentHandler handler;

        /**
         * Construct
         * 
         * @param handler ImportContentHandler
         */
        private ContentHandlerStreamHandler(ImportContentHandler handler)
        {
            this.handler = handler;
        }
        
        /* (non-Javadoc)
         * @see org.alfresco.service.cmr.view.ImportPackageHandler#startImport()
         */
        public void startImport()
        {
        }

        /* (non-Javadoc)
         * @see org.alfresco.service.cmr.view.ImportStreamHandler#importStream(java.lang.String)
         */
        public InputStream importStream(String content)
        {
            return handler.importStream(content);
        }

        /* (non-Javadoc)
         * @see org.alfresco.service.cmr.view.ImportPackageHandler#getDataStream()
         */
        public Reader getDataStream()
        {
            return null;
        }

        /* (non-Javadoc)
         * @see org.alfresco.service.cmr.view.ImportPackageHandler#endImport()
         */
        public void endImport()
        {
        }
    }
}
