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

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.node.MLPropertyInterceptor;
import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.service.cmr.dictionary.AssociationDefinition;
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.PropertyDefinition;
import org.alfresco.service.cmr.repository.AssociationRef;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.MLText;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.Path;
import org.alfresco.service.cmr.repository.datatype.TypeConversionException;
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.AuthenticationService;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.cmr.view.ExportPackageHandler;
import org.alfresco.service.cmr.view.Exporter;
import org.alfresco.service.cmr.view.ExporterContext;
import org.alfresco.service.cmr.view.ExporterCrawlerParameters;
import org.alfresco.service.cmr.view.ExporterException;
import org.alfresco.service.cmr.view.ExporterService;
import org.alfresco.service.cmr.view.ImporterException;
import org.alfresco.service.cmr.view.Location;
import org.alfresco.service.cmr.view.ReferenceType;
import org.alfresco.service.descriptor.DescriptorService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;
import org.springframework.extensions.surf.util.ParameterCheck;


/**
 * Default implementation of the Exporter Service.
 * 
 * @author David Caruana
 */
public class ExporterComponent
    implements ExporterService
{
    // Supporting services
    private NamespaceService namespaceService;
    private DictionaryService dictionaryService;
    private NodeService nodeService;
    private SearchService searchService;
    private ContentService contentService;
    private DescriptorService descriptorService;
    private AuthenticationService authenticationService;
    private PermissionService permissionService;
    

    /** Indent Size */
    private int indentSize = 2;
    private boolean exportSecondaryNodes = false;
    
    
    /**
     * @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 descriptorService  the descriptor service
     */
    public void setDescriptorService(DescriptorService descriptorService)
    {
        this.descriptorService = descriptorService;
    }

    /**
     * @param authenticationService  the authentication service
     */
    public void setAuthenticationService(AuthenticationService authenticationService)
    {
        this.authenticationService = authenticationService; 
    }
    
    /**
     * @param permissionService  the permission service
     */
    public void setPermissionService(PermissionService permissionService)
    {
        this.permissionService = permissionService;
    }
    
    /**
     * @param exportSecondaryNodes whether children that do dot have a primary association with their parent are exported as nodes
     * If false, these nodes will be exported as secondary links.
     */
    public void setExportSecondaryNodes(boolean exportSecondaryNodes) 
    {
        this.exportSecondaryNodes = exportSecondaryNodes;
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.service.cmr.view.ExporterService#exportView(java.io.OutputStream, org.alfresco.service.cmr.view.ExporterCrawlerParameters, org.alfresco.service.cmr.view.Exporter)
     */
    public void exportView(OutputStream viewWriter, ExporterCrawlerParameters parameters, Exporter progress)
    {
        ParameterCheck.mandatory("View Writer", viewWriter);
        
        // Construct a basic XML Exporter
        Exporter xmlExporter = createXMLExporter(viewWriter, parameters.getReferenceType());

        // Export
        exportView(xmlExporter, parameters, progress);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.service.cmr.view.ExporterService#exportView(org.alfresco.service.cmr.view.ExportPackageHandler, org.alfresco.service.cmr.view.ExporterCrawlerParameters, org.alfresco.service.cmr.view.Exporter)
     */
    public void exportView(ExportPackageHandler exportHandler, ExporterCrawlerParameters parameters, Exporter progress)
    {
        ParameterCheck.mandatory("Stream Handler", exportHandler);

        // create exporter around export handler
        exportHandler.startExport();
        OutputStream dataFile = exportHandler.createDataStream();
        Exporter xmlExporter = createXMLExporter(dataFile, parameters.getReferenceType());
        URLExporter urlExporter = new URLExporter(xmlExporter, exportHandler);

        // export        
        exportView(urlExporter, parameters, progress);
        
        // end export
        exportHandler.endExport();
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.service.cmr.view.ExporterService#exportView(org.alfresco.service.cmr.view.Exporter, org.alfresco.service.cmr.view.ExporterCrawler, org.alfresco.service.cmr.view.Exporter)
     */
    public void exportView(Exporter exporter, ExporterCrawlerParameters parameters, Exporter progress)
    {
        ParameterCheck.mandatory("Exporter", exporter);
        
        ChainedExporter chainedExporter = new ChainedExporter(new Exporter[] {exporter, progress});
        DefaultCrawler crawler = new DefaultCrawler();
        crawler.export(parameters, chainedExporter);
    }
    
    /**
     * Create an XML Exporter that exports repository information to the specified
     * output stream in xml format.
     * 
     * @param viewWriter  the output stream to write to
     * @param referenceType  the format of references to export
     * @return  the xml exporter
     */
    private Exporter createXMLExporter(OutputStream viewWriter, ReferenceType referenceType)
    {
        // Define output format
        OutputFormat format = OutputFormat.createPrettyPrint();
        format.setNewLineAfterDeclaration(false);
        format.setIndentSize(indentSize);
        format.setEncoding("UTF-8");

        // Construct an XML Exporter
        try
        {
            XMLWriter writer = new XMLWriter(viewWriter, format);
            ViewXMLExporter exporter = new ViewXMLExporter(namespaceService, nodeService, searchService, dictionaryService, permissionService, writer);
            exporter.setReferenceType(referenceType);
            return exporter;
        }
        catch (UnsupportedEncodingException e)        
        {
            throw new ExporterException("Failed to create XML Writer for export", e);            
        }
        catch (Exception e)        
        {
            throw new ExporterException("Failed to create XML Writer for export", e);            
        }
    }
    
    
    /**
     * Responsible for navigating the Repository from specified location and invoking
     * the provided exporter call-back for the actual export implementation.
     * 
     * @author David Caruana
     */
    private class DefaultCrawler implements ExporterCrawler
    {
        private ExporterContextImpl context;
        
        /* (non-Javadoc)
         * @see org.alfresco.service.cmr.view.ExporterCrawler#export(org.alfresco.service.cmr.view.Exporter)
         */
        public void export(ExporterCrawlerParameters parameters, Exporter exporter)
        {
            // Initialise Crawler
            context = new ExporterContextImpl(parameters);
            exporter.start(context);

            //
            // Export Nodes
            //
            
            while (context.canRetrieve())
            {
                // determine if root repository node
                NodeRef nodeRef = context.getExportOf();
                if (parameters.isCrawlSelf())
                {
                    // export root node of specified export location
                    walkStartNamespaces(parameters, exporter);
                    boolean rootNode = nodeService.getRootNode(nodeRef.getStoreRef()).equals(nodeRef);
                    walkNode(nodeRef, parameters, exporter, rootNode);
                    walkEndNamespaces(parameters, exporter);
                }
                else if (parameters.isCrawlChildNodes())
                {
                    // export child nodes only
                    List<ChildAssociationRef> childAssocs = nodeService.getChildAssocs(nodeRef);
                    for (ChildAssociationRef childAssoc : childAssocs)
                    {
                        walkStartNamespaces(parameters, exporter);
                        walkNode(childAssoc.getChildRef(), parameters, exporter, false);
                        walkEndNamespaces(parameters, exporter);
                    }
                }

                context.setNextValue();
            }

            //
            // Export associations between nodes
            //
            context.resetContext();
            while (context.canRetrieve())
            {
                Set<NodeRef> nodesWithSecondaryLinks = context.getNodesWithSecondaryLinks();
                if (nodesWithSecondaryLinks != null)
                {
                    //
                    // Export Secondary Links between Nodes
                    //
                    for (NodeRef nodeWithAssociations : nodesWithSecondaryLinks)
                    {
                        walkStartNamespaces(parameters, exporter);
                        walkNodeSecondaryLinks(nodeWithAssociations, parameters, exporter);
                        walkEndNamespaces(parameters, exporter);
                    }
                }

                Set<NodeRef> nodesWithAssociations = context.getNodesWithAssociations();
                if (nodesWithAssociations != null)
                {
                    //
                    // Export Associations between Nodes
                    //
                    for (NodeRef nodeWithAssociations : nodesWithAssociations)
                    {
                        walkStartNamespaces(parameters, exporter);
                        walkNodeAssociations(nodeWithAssociations, parameters, exporter);
                        walkEndNamespaces(parameters, exporter);
                    }
                }
                
                context.setNextValue();
            }

            exporter.end();
        }
        
        /**
         * Call-backs for start of Namespace scope
         */
        private void walkStartNamespaces(ExporterCrawlerParameters parameters, Exporter exporter)
        {
            Collection<String> prefixes = namespaceService.getPrefixes();
            for (String prefix : prefixes)
            {
                if (!prefix.equals("xml"))
                {
                    String uri = namespaceService.getNamespaceURI(prefix);
                    exporter.startNamespace(prefix, uri);
                }
            }
        }
        
        /**
         * Call-backs for end of Namespace scope
         */
        private void walkEndNamespaces(ExporterCrawlerParameters parameters, Exporter exporter)
        {
            Collection<String> prefixes = namespaceService.getPrefixes();
            for (String prefix : prefixes)
            {
                if (!prefix.equals("xml"))
                {
                    exporter.endNamespace(prefix);
                }
            }
        }
        
        /**
         * Navigate a Node.
         * 
         * @param nodeRef  the node to navigate
         */
        private void walkNode(NodeRef nodeRef, ExporterCrawlerParameters parameters, Exporter exporter, boolean exportAsRef)
        {
            // Export node (but only if it's not excluded from export)
            QName type = nodeService.getType(nodeRef);
            if (isExcludedURI(parameters.getExcludeNamespaceURIs(), type.getNamespaceURI()))
            {
                return;
            }
            
            // explicitly included ?
            if (parameters.getIncludedPaths() != null)
            {
                String nodePathPrefixString = nodeService.getPath(nodeRef).toPrefixString(namespaceService);
                if (! (isIncludedPath(parameters.getIncludedPaths(), nodePathPrefixString)))
                {
                    return;
                }
            }

            // export node as reference to node, or as the actual node
            if (exportAsRef)
            {
                exporter.startReference(nodeRef, null);
            }
            else
            {
                exporter.startNode(nodeRef);
            }

            // Export node aspects
            exporter.startAspects(nodeRef);
            Set<QName> aspects = nodeService.getAspects(nodeRef);
            for (QName aspect : aspects)
            {
                if (isExcludedURI(parameters.getExcludeNamespaceURIs(), aspect.getNamespaceURI()))
                {
                    continue;
                }
                else if (isExcludedAspect(parameters.getExcludeAspects(), aspect))
                {
                    continue;
                }
                else
                {
                    exporter.startAspect(nodeRef, aspect);
                    exporter.endAspect(nodeRef, aspect);
                }
            }
            exporter.endAspects(nodeRef);
            
            // Export node permissions
            AccessStatus readPermission = permissionService.hasPermission(nodeRef, PermissionService.READ_PERMISSIONS);
            if (authenticationService.isCurrentUserTheSystemUser() || readPermission.equals(AccessStatus.ALLOWED))
            {
                Set<AccessPermission> permissions = permissionService.getAllSetPermissions(nodeRef);
                boolean inheritPermissions = permissionService.getInheritParentPermissions(nodeRef);
                if (permissions.size() > 0 || !inheritPermissions)
                {
                    exporter.startACL(nodeRef);
                    for (AccessPermission permission : permissions)
                    {
                        if(permission.isSetDirectly())
                        {
                            exporter.permission(nodeRef, permission);
                        }
                    }
                    exporter.endACL(nodeRef);
                }
            }
            
            // Export node properties
            exporter.startProperties(nodeRef);
            boolean aware = MLPropertyInterceptor.setMLAware(true);
            Map<QName, Serializable> properties = nodeService.getProperties(nodeRef);
            MLPropertyInterceptor.setMLAware(aware);
            for (QName property : properties.keySet())
            {
                // filter out properties whose namespace is excluded
                if (isExcludedURI(parameters.getExcludeNamespaceURIs(), property.getNamespaceURI()))
                {
                    continue;
                }
                if (isExcludedAspectProperty(parameters.getExcludeAspects(), property))
                {
                    continue;
                }
                
                // filter out properties whose value is null, if not required
                Object value = properties.get(property);
                if (!parameters.isCrawlNullProperties() && value == null)
                {
                    continue;
                }
                
                // start export of property
                exporter.startProperty(nodeRef, property);

                if (value instanceof Collection)
                {
                    exporter.startValueCollection(nodeRef, property);
                    int index = 0;
                    for (Object valueInCollection : (Collection)value)
                    {
                        walkProperty(nodeRef, property, valueInCollection, index, parameters, exporter);
                        index++;
                    }
                    exporter.endValueCollection(nodeRef, property);
                }
                else
                {
                    if (value instanceof MLText)
                    {
                        MLText valueMLT = (MLText) value;
                        Set<Locale> locales = valueMLT.getLocales();
                        for (Locale locale : locales)
                        {
                            String localeValue = valueMLT.getValue(locale);
                            exporter.startValueMLText(nodeRef, locale, localeValue == null);
                            walkProperty(nodeRef, property, localeValue, -1, parameters, exporter);
                            exporter.endValueMLText(nodeRef);
                        }
                    }
                    else
                    {
                        walkProperty(nodeRef, property, value, -1, parameters, exporter);
                    }
                }

                // end export of property
                exporter.endProperty(nodeRef, property);
            }
            exporter.endProperties(nodeRef);
            
            // Export node children
            if (parameters.isCrawlChildNodes())
            {
                // sort associations into assoc type buckets filtering out unneccessary associations
                Map<QName, List<ChildAssociationRef>> assocTypes = new HashMap<QName, List<ChildAssociationRef>>();
                List<ChildAssociationRef> childAssocs = nodeService.getChildAssocs(nodeRef);
                for (ChildAssociationRef childAssoc : childAssocs)
                {
                    QName childAssocType = childAssoc.getTypeQName();
                    if (isExcludedURI(parameters.getExcludeNamespaceURIs(), childAssocType.getNamespaceURI()))
                    {
                        continue;
                    }
                    if (isExcludedChildAssoc(parameters.getExcludeChildAssocs(), childAssocType))
                    {
                        continue;
                    }
                    if (isExcludedAspectAssociation(parameters.getExcludeAspects(), childAssocType))
                    {
                        continue;
                    }
                    if (childAssoc.isPrimary() == false && !exportSecondaryNodes)
                    {
                        context.recordSecondaryLink(nodeRef);
                        continue;
                    }
                    if (isExcludedURI(parameters.getExcludeNamespaceURIs(), childAssoc.getQName().getNamespaceURI()))
                    {
                        continue;
                    }
                    
                    List<ChildAssociationRef> assocRefs = assocTypes.get(childAssocType);
                    if (assocRefs == null)
                    {
                        assocRefs = new ArrayList<ChildAssociationRef>();
                        assocTypes.put(childAssocType, assocRefs);
                    }
                    assocRefs.add(childAssoc);
                }
                
                // output each association type bucket
                if (assocTypes.size() > 0)
                {
                    exporter.startAssocs(nodeRef);
                    for (Map.Entry<QName, List<ChildAssociationRef>> assocType : assocTypes.entrySet())
                    {
                        List<ChildAssociationRef> assocRefs = assocType.getValue();
                        if (assocRefs.size() > 0)
                        {
                            exporter.startAssoc(nodeRef, assocType.getKey());
                            for (ChildAssociationRef assocRef : assocRefs)
                            {
                                walkNode(assocRef.getChildRef(), parameters, exporter, false);
                            }
                            exporter.endAssoc(nodeRef, assocType.getKey());
                        }
                    }
                    exporter.endAssocs(nodeRef);
                }
            }
            
            // Export node associations
            if (parameters.isCrawlAssociations())
            {
                List<AssociationRef> associations = nodeService.getTargetAssocs(nodeRef, RegexQNamePattern.MATCH_ALL);
                if (associations.size() > 0)
                {
                    context.recordAssociation(nodeRef);
                }
            }
            
            // Signal end of node
            // export node as reference to node, or as the actual node
            if (exportAsRef)
            {
                exporter.endReference(nodeRef);
            }
            else
            {
                exporter.endNode(nodeRef);
            }
        }
        
        /**
         * Export Property
         * 
         * @param nodeRef NodeRef
         * @param property QName
         * @param value Object
         * @param index int
         * @param parameters ExporterCrawlerParameters
         * @param exporter Exporter
         */
        private void walkProperty(NodeRef nodeRef, QName property, Object value, int index, ExporterCrawlerParameters parameters, Exporter exporter)
        {
            // determine data type of value
            PropertyDefinition propDef = dictionaryService.getProperty(property);
            DataTypeDefinition dataTypeDef = (propDef == null) ? null : propDef.getDataType();
            QName valueDataType = null;
            if (dataTypeDef == null || dataTypeDef.getName().equals(DataTypeDefinition.ANY))
            {
                dataTypeDef = (value == null) ? null : dictionaryService.getDataType(value.getClass());
                if (dataTypeDef != null)
                {
                    valueDataType = dataTypeDef.getName();
                }
            }
            else
            {
                valueDataType = dataTypeDef.getName();
            }

            if (valueDataType == null || !valueDataType.equals(DataTypeDefinition.CONTENT))
            {
                // Export non content data types
                try
                {
                    exporter.value(nodeRef, property, value, index);
                }
                catch(TypeConversionException e)
                {
                    exporter.warning("Value of property " + property + " could not be converted to xml string");
                    exporter.value(nodeRef, property, (value == null ? null : value.toString()), index);
                }
            }
            else
            {
                // export property of datatype CONTENT
                ContentReader reader = contentService.getReader(nodeRef, property);
                if (!parameters.isCrawlContent() || reader == null || reader.exists() == false)
                {
                    // export an empty url for the content
                    ContentData contentData = (ContentData)value;
                    ContentData noContentURL = null;
                    if (contentData == null)
                    {
                        noContentURL = new ContentData("", null, 0L, "UTF-8");
                    }
                    else
                    {
                        noContentURL = new ContentData("", contentData.getMimetype(), contentData.getSize(), contentData.getEncoding());
                    }
                    exporter.content(nodeRef, property, null, noContentURL, index);
                    exporter.warning("Skipped content for property " + property + " on node " + nodeRef);
                }
                else
                {
                    InputStream inputStream = reader.getContentInputStream();
                    try
                    {
                        exporter.content(nodeRef, property, inputStream, reader.getContentData(), index);
                    }
                    finally
                    {
                        try
                        {
                            inputStream.close();
                        }
                        catch(IOException e)
                        {
                            throw new ExporterException("Failed to export node content for node " + nodeRef, e);
                        }
                    }
                }
            }
        }
        
        /**
         * Export Secondary Links
         * 
         * @param nodeRef NodeRef
         * @param parameters ExporterCrawlerParameters
         * @param exporter Exporter
         */
        private void walkNodeSecondaryLinks(NodeRef nodeRef, ExporterCrawlerParameters parameters, Exporter exporter)
        {
            // sort associations into assoc type buckets filtering out unneccessary associations
            Map<QName, List<ChildAssociationRef>> assocTypes = new HashMap<QName, List<ChildAssociationRef>>();
            List<ChildAssociationRef> childAssocs = nodeService.getChildAssocs(nodeRef);
            for (ChildAssociationRef childAssoc : childAssocs)
            {
                // determine if child association should be exported
                QName childAssocType = childAssoc.getTypeQName();
                if (isExcludedURI(parameters.getExcludeNamespaceURIs(), childAssocType.getNamespaceURI()))
                {
                    continue;
                }
                if (isExcludedChildAssoc(parameters.getExcludeChildAssocs(), childAssocType))
                {
                    continue;
                }
                if (isExcludedAspectAssociation(parameters.getExcludeAspects(), childAssocType))
                {
                    continue;
                }
                if (childAssoc.isPrimary())
                {
                    continue;
                }
                if (!isWithinExport(childAssoc.getChildRef(), parameters))
                {
                    continue;
                }
                
                List<ChildAssociationRef> assocRefs = assocTypes.get(childAssocType);
                if (assocRefs == null)
                {
                    assocRefs = new ArrayList<ChildAssociationRef>();
                    assocTypes.put(childAssocType, assocRefs);
                }
                assocRefs.add(childAssoc);
            }
            
            // output each association type bucket
            if (assocTypes.size() > 0)
            {
                exporter.startReference(nodeRef, null);
                exporter.startAssocs(nodeRef);
                for (Map.Entry<QName, List<ChildAssociationRef>> assocType : assocTypes.entrySet())
                {
                    List<ChildAssociationRef> assocRefs = assocType.getValue();
                    if (assocRefs.size() > 0)
                    {
                        exporter.startAssoc(nodeRef, assocType.getKey());
                        for (ChildAssociationRef assocRef : assocRefs)
                        {
                            exporter.startReference(assocRef.getChildRef(), assocRef.getQName());
                            exporter.endReference(assocRef.getChildRef());
                        }
                        exporter.endAssoc(nodeRef, assocType.getKey());
                    }
                }
                exporter.endAssocs(nodeRef);
                exporter.endReference(nodeRef);
            }
        }
        
        /**
         * Export Node Associations
         * 
         * @param nodeRef NodeRef
         * @param parameters ExporterCrawlerParameters
         * @param exporter Exporter
         */
        private void walkNodeAssociations(NodeRef nodeRef, ExporterCrawlerParameters parameters, Exporter exporter)
        {
            // sort associations into assoc type buckets filtering out unneccessary associations
            Map<QName, List<AssociationRef>> assocTypes = new HashMap<QName, List<AssociationRef>>();
            List<AssociationRef> assocs = nodeService.getTargetAssocs(nodeRef, RegexQNamePattern.MATCH_ALL);
            for (AssociationRef assoc : assocs)
            {
                QName assocType = assoc.getTypeQName();
                if (isExcludedURI(parameters.getExcludeNamespaceURIs(), assocType.getNamespaceURI()))
                {
                    continue;
                }
                if (!isWithinExport(assoc.getTargetRef(), parameters))
                {
                    continue;
                }
                
                List<AssociationRef> assocRefs = assocTypes.get(assocType);
                if (assocRefs == null)
                {
                    assocRefs = new ArrayList<AssociationRef>();
                    assocTypes.put(assocType, assocRefs);
                }
                assocRefs.add(assoc);
            }
            
            // output each association type bucket
            if (assocTypes.size() > 0)
            {
                exporter.startReference(nodeRef, null);
                exporter.startAssocs(nodeRef);
                for (Map.Entry<QName, List<AssociationRef>> assocType : assocTypes.entrySet())
                {
                    List<AssociationRef> assocRefs = assocType.getValue();
                    if (assocRefs.size() > 0)
                    {
                        exporter.startAssoc(nodeRef, assocType.getKey());
                        for (AssociationRef assocRef : assocRefs)
                        {
                            exporter.startReference(assocRef.getTargetRef(), null);
                            exporter.endReference(assocRef.getTargetRef());
                        }
                        exporter.endAssoc(nodeRef, assocType.getKey());
                    }
                }
                exporter.endAssocs(nodeRef);
                exporter.endReference(nodeRef);
            }
        }

        /**
         * Is the specified URI an excluded URI?
         * 
         * @param uri  the URI to test
         * @return  true => it's excluded from the export
         */
        private boolean isExcludedURI(String[] excludeNamespaceURIs, String uri)
        {
            for (String excludedURI : excludeNamespaceURIs)
            {
                if (uri.equals(excludedURI))
                {
                    return true;
                }
            }
            return false;
        }
        
        private boolean isIncludedPath(String[] includedPaths, String path)
        {
            for (String includePath : includedPaths)
            {
                // note: allow parents or children - e.g. if included path is /a/b/c then /, /a, /a/b, /a/b/c, /a/b/c/d, /a/b/c/d/e are all included         	 
                if (includePath.startsWith(path) || path.startsWith(includePath))
                {
                    return true;
                }
            }

            return false;
        }
        
        
        /**
         * Is the aspect unexportable?
         * 
         * @param aspectQName           the aspect name
         * @return                      <tt>true</tt> if the aspect can't be exported
         */
        private boolean isExcludedAspect(QName[] excludeAspects, QName aspectQName)
        {
            if (aspectQName.equals(ContentModel.ASPECT_MULTILINGUAL_DOCUMENT) ||
                    aspectQName.equals(ContentModel.ASPECT_MULTILINGUAL_EMPTY_TRANSLATION))
            {
                return true;
            }
            else
            {
                for (QName excludeAspect : excludeAspects)
                {
                    if (aspectQName.equals(excludeAspect))
                    {
                        return true;
                    }
                }
            }
            return false;
        }

        /**
         * Is the child association unexportable?
         * 
         * @param childAssocQName           the child assoc name
         * @return                      <tt>true</tt> if the aspect can't be exported
         */
        private boolean isExcludedChildAssoc(QName[] excludeChildAssocs, QName childAssocQName)
        {
            for (QName excludeChildAssoc : excludeChildAssocs)
            {
                if (childAssocQName.equals(excludeChildAssoc))
                {
                    return true;
                }
            }
            return false;
        }
        
        /**
         * Is the property unexportable?
         */
        private boolean isExcludedAspectProperty(QName[] excludeAspects, QName propertyQName)
        {
            PropertyDefinition propDef = dictionaryService.getProperty(propertyQName);
            if (propDef == null)
            {
                return false;
            }
            
            ClassDefinition classDef = propDef.getContainerClass();
            if (classDef == null || !classDef.isAspect())
            {
                return false;
            }
            
            return isExcludedAspect(excludeAspects, classDef.getName());
        }

        /**
         * Is the association unexportable?
         */
        private boolean isExcludedAspectAssociation(QName[] excludeAspects, QName associationQName)
        {
            AssociationDefinition assocDef = dictionaryService.getAssociation(associationQName);
            if (assocDef == null)
            {
                return false;
            }
            
            ClassDefinition classDef = assocDef.getSourceClass();
            if (classDef == null || !classDef.isAspect())
            {
                return false;
            }
            
            return isExcludedAspect(excludeAspects, classDef.getName());
        }
        
        /**
         * Determine if specified Node Reference is within the set of nodes to be exported
         * 
         * @param nodeRef  node reference to check
         * @return  true => node reference is within export set
         */
        private boolean isWithinExport(NodeRef nodeRef, ExporterCrawlerParameters parameters)
        {
            boolean isWithin = false;

            try
            {
                // Current strategy is to determine if node is a child of the root exported node
                for (NodeRef exportRoot : context.getExportList())
                {
                    if (nodeRef.equals(exportRoot) && parameters.isCrawlSelf() == true)
                    {
                        // node to export is the root export node (and root is to be exported)
                        isWithin = true;
                    }
                    else
                    {
                        // locate export root in primary parent path of node
                        Path nodePath = nodeService.getPath(nodeRef);
                        for (int i = nodePath.size() - 1; i >= 0; i--)
                        {
                            Path.ChildAssocElement pathElement = (Path.ChildAssocElement) nodePath.get(i);
                            if (pathElement.getRef().getChildRef().equals(exportRoot))
                            {
                                isWithin = true;
                                break;
                            }
                        }
                    }
                }
            }
            catch (AccessDeniedException accessErr)
            {
                // use default if this occurs
            }
            catch (InvalidNodeRefException nodeErr)
            {
                // use default if this occurs
            }

            return isWithin;
        }
    }


    /**
     * Exporter Context
     */
    private class ExporterContextImpl implements ExporterContext
    {
        private NodeRef[] exportList;
        private NodeRef[] parentList;
        private String exportedBy;
        private Date exportedDate;
        private String exporterVersion;
        
        private Map<Integer, Set<NodeRef>> nodesWithSecondaryLinks = new HashMap<Integer, Set<NodeRef>>();
        private Map<Integer, Set<NodeRef>> nodesWithAssociations = new HashMap<Integer, Set<NodeRef>>();
        
        private int index;
        
        
        /**
         * Construct
         * 
         * @param parameters  exporter crawler parameters
         */
        public ExporterContextImpl(ExporterCrawlerParameters parameters)
        {
            index = 0;
            
            // get current user performing export
            String currentUserName = authenticationService.getCurrentUserName();
            exportedBy = (currentUserName == null) ? "unknown" : currentUserName;

            // get current date
            exportedDate = new Date(System.currentTimeMillis());

            // get list of exported nodes
            exportList = (parameters.getExportFrom() == null) ? null : parameters.getExportFrom().getNodeRefs();
            if (exportList == null)
            {
                // multi-node export
                exportList = new NodeRef[1];
                NodeRef exportOf = getNodeRef(parameters.getExportFrom());
                exportList[0] = exportOf;
            }
            parentList = new NodeRef[exportList.length];
            for (int i = 0; i < exportList.length; i++)
            {
                parentList[i] = getParent(exportList[i], parameters.isCrawlSelf());
            }
            
            // get exporter version
            exporterVersion = descriptorService.getServerDescriptor().getVersion();
        }
        
        public boolean canRetrieve()
        {
            return index < exportList.length;
        }
        
        public int setNextValue()
        {
            return ++index;
        }
        
        public void resetContext()
        {
            index = 0;
        }
        
        /* (non-Javadoc)
         * @see org.alfresco.service.cmr.view.ExporterContext#getExportedBy()
         */
        public String getExportedBy()
        {
            return exportedBy;
        }

        /* (non-Javadoc)
         * @see org.alfresco.service.cmr.view.ExporterContext#getExportedDate()
         */
        public Date getExportedDate()
        {
            return exportedDate;
        }

        /* (non-Javadoc)
         * @see org.alfresco.service.cmr.view.ExporterContext#getExporterVersion()
         */
        public String getExporterVersion()
        {
            return exporterVersion;
        }

        /* (non-Javadoc)
         * @see org.alfresco.service.cmr.view.ExporterContext#getExportOf()
         */
        public NodeRef getExportOf()
        {
            if (canRetrieve())
            {
                return exportList[index];
            }
            return null;
        }
        
        /*
         * (non-Javadoc)
         * @see org.alfresco.service.cmr.view.ExporterContext#getExportParent()
         */
        public NodeRef getExportParent()
        {
            if (canRetrieve())
            {
                return parentList[index];
            }
            return null;
        }
        
        /*
         * (non-Javadoc)
         * @see org.alfresco.service.cmr.view.ExporterContext#getExportList()
         */
        public NodeRef[] getExportList()
        {
            return exportList;
        }

        /*
         * (non-Javadoc)
         * @see org.alfresco.service.cmr.view.ExporterContext#getExportParentList()
         */
        public NodeRef[] getExportParentList()
        {
            return parentList;
        }

        /**
         * Record that associations exist for node
         * 
         * @param nodeRef NodeRef
         */
        public void recordAssociation(NodeRef nodeRef)
        {
            Set<NodeRef> nodes = nodesWithAssociations.get(index);
            if (nodes == null)
            {
                nodes = new HashSet<NodeRef>();
                nodesWithAssociations.put(index, nodes);
            }
            nodes.add(nodeRef);
        }

        /**
         * Gets nodes that have been recorded with associations
         * 
         * @return Set<NodeRef>
         */
        public Set<NodeRef> getNodesWithAssociations()
        {
            Set<NodeRef> nodes = nodesWithAssociations.get(index);
            if (nodes != null)
            {
                return nodes;
            }
            return null;
        }

        /**
         * Record that secondary links exist for node
         * 
         * @param nodeRef NodeRef
         */
        public void recordSecondaryLink(NodeRef nodeRef)
        {
            Set<NodeRef> nodes = nodesWithSecondaryLinks.get(index);
            if (nodes == null)
            {
                nodes = new HashSet<NodeRef>();
                nodesWithSecondaryLinks.put(index, nodes);
            }
            nodes.add(nodeRef);
        }

        /**
         * Gets nodes that have been recorded with secondary links
         * 
         * @return
         */
        public Set<NodeRef> getNodesWithSecondaryLinks()
        {
            Set<NodeRef> nodes = nodesWithSecondaryLinks.get(index);
            if (nodes != null)
            {
                return nodes;
            }
            return null;
        }
        
        /**
         * Get the Node Ref from the specified Location
         * 
         * @param location  the location
         * @return  the node reference
         */
        private NodeRef getNodeRef(Location location)
        {
            ParameterCheck.mandatory("Location", location);
        
            // Establish node to export from
            NodeRef nodeRef = (location == null) ? null : 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 == null) ? null : location.getPath();
            if (path != null && path.length() >0)
            {
                // Create a valid path and search
                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;
        }
     
        /**
         * Gets the parent node of the items to be exported
         * 
         * @param exportOf NodeRef
         * @param exportSelf boolean
         * @return NodeRef
         */
        private NodeRef getParent(NodeRef exportOf, boolean exportSelf)
        {
            NodeRef parent = null;
            
            if (exportSelf)
            {
                NodeRef rootNode = nodeService.getRootNode(exportOf.getStoreRef());
                if (rootNode.equals(exportOf))
                {
                    parent = exportOf;
                }
                else
                {
                    ChildAssociationRef parentRef = nodeService.getPrimaryParent(exportOf);
                    parent = parentRef.getParentRef();
                }
            }
            else
            {
                parent = exportOf;
            }
            
            return parent;
        }

    }
    
}
