/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see .
 * #L%
 */
package org.alfresco.repo.virtual.bundle;
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.download.DownloadModel;
import org.alfresco.repo.node.archive.NodeArchiveService;
import org.alfresco.repo.node.db.traitextender.NodeServiceExtension;
import org.alfresco.repo.node.db.traitextender.NodeServiceTrait;
import org.alfresco.repo.virtual.ActualEnvironment;
import org.alfresco.repo.virtual.ActualEnvironmentException;
import org.alfresco.repo.virtual.VirtualContentModel;
import org.alfresco.repo.virtual.VirtualizationException;
import org.alfresco.repo.virtual.config.NodeRefExpression;
import org.alfresco.repo.virtual.ref.GetActualNodeRefMethod;
import org.alfresco.repo.virtual.ref.GetAspectsMethod;
import org.alfresco.repo.virtual.ref.GetParentReferenceMethod;
import org.alfresco.repo.virtual.ref.NodeProtocol;
import org.alfresco.repo.virtual.ref.Protocols;
import org.alfresco.repo.virtual.ref.Reference;
import org.alfresco.repo.virtual.store.VirtualStore;
import org.alfresco.repo.virtual.template.FilingData;
import org.alfresco.service.cmr.dictionary.InvalidAspectException;
import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.repository.AssociationRef;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.InvalidChildAssociationRefException;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.InvalidStoreRefException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeRef.Status;
import org.alfresco.service.cmr.repository.Path;
import org.alfresco.service.cmr.repository.StoreExistsException;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.QNamePattern;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class VirtualNodeServiceExtension extends VirtualSpringBeanExtension
            implements NodeServiceExtension
{
    private static Log logger = LogFactory.getLog(VirtualNodeServiceExtension.class);
    private VirtualStore smartStore;
    private ActualEnvironment environment;
    private NodeRefExpression downloadAssociationsFolder;
    public VirtualNodeServiceExtension()
    {
        super(NodeServiceTrait.class);
    }
    public void setEnvironment(ActualEnvironment environment)
    {
        this.environment = environment;
    }
    public void setSmartStore(VirtualStore smartStore)
    {
        this.smartStore = smartStore;
    }
    @Override
    public boolean hasAspect(NodeRef nodeRef, QName aspectQName)
    {
        if (Reference.isReference(nodeRef))
        {
            boolean isNodeProtocol = Reference.fromNodeRef(nodeRef).getProtocol().equals(Protocols.NODE.protocol);
            if (VirtualContentModel.ASPECT_VIRTUAL.equals(aspectQName) || ContentModel.ASPECT_TITLED.equals(aspectQName))
            {
                return !isNodeProtocol;
            }
            else if (VirtualContentModel.ASPECT_VIRTUAL_DOCUMENT.equals(aspectQName))
            {
                return isNodeProtocol;
            }
            else
            {
                Reference reference = Reference.fromNodeRef(nodeRef);
                NodeRef actualNodeRef = reference.execute(new GetActualNodeRefMethod(environment));
                return getTrait().hasAspect(actualNodeRef,
                                            aspectQName);
            }
        }
        else
        {
            return getTrait().hasAspect(nodeRef,
                                        aspectQName);
        }
    }
    @Override
    public QName getType(NodeRef nodeRef)
    {
        if (Reference.isReference(nodeRef))
        {
            QName type = smartStore.getType(Reference.fromNodeRef(nodeRef));
            return type;
        }
        else
        {
            return getTrait().getType(nodeRef);
        }
    }
    private Map getVirtualProperties(Reference reference)
    {
        return smartStore.getProperties(reference);
    }
    @Override
    public Map getProperties(NodeRef nodeRef)
    {
        if (Reference.isReference(nodeRef))
        {
            return getVirtualProperties(Reference.fromNodeRef(nodeRef));
        }
        else
        {
            return getTrait().getProperties(nodeRef);
        }
    }
    @Override
    public Serializable getProperty(NodeRef nodeRef, QName qname)
    {
        if (Reference.isReference(nodeRef))
        {
            return getVirtualProperties(Reference.fromNodeRef(nodeRef)).get(qname);
        }
        else
        {
            return getTrait().getProperty(nodeRef,
                                          qname);
        }
    }
    @Override
    public Set getAspects(NodeRef nodeRef)
    {
        NodeServiceTrait theTrait = getTrait();
        if (Reference.isReference(nodeRef))
        {
            Reference vRef = Reference.fromNodeRef(nodeRef);
            GetAspectsMethod method = new GetAspectsMethod(theTrait,
                                                           environment);
            return vRef.execute(method);
        }
        else
        {
            return theTrait.getAspects(nodeRef);
        }
    }
    @Override
    public Path getPath(NodeRef nodeRef)
    {
        if (Reference.isReference(nodeRef))
        {
            return Reference.fromNodeRef(nodeRef).execute(new GetPathMethod(smartStore,
                                                                            environment));
        }
        else
        {
            return getTrait().getPath(nodeRef);
        }
    }
    @Override
    public List getPaths(NodeRef nodeRef, boolean primaryOnly)
    {
        if (Reference.isReference(nodeRef))
        {
            Reference reference = Reference.fromNodeRef(nodeRef);
            NodeRef actualNodeRef = reference.execute(new GetActualNodeRefMethod(environment));
            return getTrait().getPaths(actualNodeRef,
                                       primaryOnly);
        }
        else
        {
            return getTrait().getPaths(nodeRef,
                                       primaryOnly);
        }
    }
    @Override
    public boolean exists(NodeRef nodeRef)
    {
        if (Reference.isReference(nodeRef))
        {
            // For now references last forever (i.e. there is no expiration
            // mechanism )
            return true;
        }
        else
        {
            return getTrait().exists(nodeRef);
        }
    }
    @Override
    public ChildAssociationRef createNode(NodeRef parentRef, QName assocTypeQName, QName assocQName,
                QName nodeTypeQName)
    {
        if (Reference.isReference(parentRef))
        {
            return createNode(parentRef,
                              assocTypeQName,
                              assocQName,
                              nodeTypeQName,
                              Collections. emptyMap());
        }
        else
        {
            QName materialAssocQName = materializeAssocQName(assocQName);
            return getTrait().createNode(parentRef,
                                         assocTypeQName,
                                         materialAssocQName,
                                         nodeTypeQName);
        }
    }
    @Override
    public ChildAssociationRef createNode(NodeRef parentRef, QName assocTypeQName, QName assocQName,
                QName nodeTypeQName, Map properties)
    {
        NodeServiceTrait theTrait = getTrait();
        if (Reference.isReference(parentRef) && !isVirtualContextFolder(parentRef,
                                                                        environment))
        {
            // CM-533 Suppress options to create folders in a virtual folder
            // (repo)
            if (environment.isSubClass(nodeTypeQName,
                                       ContentModel.TYPE_FOLDER))
            {
                throw new VirtualizationException("The creation of folders within virtual folders is disabled.");
            }
            try
            {
                Reference parentReference = Reference.fromNodeRef(parentRef);
                FilingData filingData = smartStore.createFilingData(parentReference,
                                                                      assocTypeQName,
                                                                      assocQName,
                                                                      nodeTypeQName,
                                                                      properties);
                NodeRef childParentNodeRef = filingData.getFilingNodeRef();
                if (childParentNodeRef != null)
                {
                    Map filingDataProperties = filingData.getProperties();
                    QName changedAssocQName = assocQName;
                    if (filingDataProperties.containsKey(ContentModel.PROP_NAME))
                    {
                        String fileName = (String) filingDataProperties.get(ContentModel.PROP_NAME);
                        String changedFileName = handleExistingFile(childParentNodeRef,
                                                                    fileName);
                        if (!changedFileName.equals(fileName))
                        {
                            filingDataProperties.put(ContentModel.PROP_NAME,
                                                     changedFileName);
                            QName filingDataAssocQName = filingData.getAssocQName();
                            changedAssocQName = QName.createQName(filingDataAssocQName.getNamespaceURI(),
                                                                  QName.createValidLocalName(changedFileName));
                        }
                    }
                    ChildAssociationRef actualChildAssocRef = theTrait.createNode(childParentNodeRef,
                                                                                  filingData.getAssocTypeQName(),
                                                                                  changedAssocQName == null
                                                                                              ? filingData.getAssocQName()
                                                                                              : changedAssocQName,
                                                                                  filingData.getNodeTypeQName(),
                                                                                  filingDataProperties);
                    Reference nodeProtocolChildRef = NodeProtocol.newReference(actualChildAssocRef.getChildRef(),
                                                                               parentReference);
                    QName vChildAssocQName = QName
                                .createQNameWithValidLocalName(VirtualContentModel.VIRTUAL_CONTENT_MODEL_1_0_URI,
                                                               actualChildAssocRef.getQName().getLocalName());
                    ChildAssociationRef childAssocRef = new ChildAssociationRef(actualChildAssocRef.getTypeQName(),
                                                                                parentRef,
                                                                                vChildAssocQName,
                                                                                nodeProtocolChildRef.toNodeRef());
                    Set aspects = filingData.getAspects();
                    for (QName aspect : aspects)
                    {
                        theTrait.addAspect(actualChildAssocRef.getChildRef(),
                                           aspect,
                                           filingDataProperties);
                    }
                    return childAssocRef;
                }
                else
                {
                    throw new InvalidNodeRefException("Can not create node using parent ",
                                                      parentRef);
                }
            }
            catch (VirtualizationException e)
            {
                throw new InvalidNodeRefException("Could not create node in virtual context.",
                                                  parentRef,
                                                  e);
            }
        }
        else
        {
            QName materialAssocQName = materializeAssocQName(assocQName);
            if (isVirtualContextFolder(parentRef,
                                       environment))
            {
                parentRef = smartStore.materializeIfPossible(parentRef);
            }
            return theTrait.createNode(parentRef,
                                       assocTypeQName,
                                       materialAssocQName,
                                       nodeTypeQName,
                                       properties);
        }
    }
    private QName materializeAssocQName(QName assocQName)
    {
        // Version nodes have too long assocQNames so we try
        // to detect references with "material" protocols in order to
        // replace the assocQNames with material correspondents.
        try
        {
            String lName = assocQName.getLocalName();
            NodeRef nrAssocQName = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE,
                                               lName);
            if (Reference.isReference(nrAssocQName))
            {
                nrAssocQName = smartStore.materializeIfPossible(nrAssocQName);
                QName materialAssocQName = QName.createQName(assocQName.getNamespaceURI(),
                                                             nrAssocQName.getId());
                return materialAssocQName;
            }
            else
            {
                return assocQName;
            }
        }
        catch (VirtualizationException e)
        {
            // We assume it can not be put through the
            // isReference-virtualize-materialize.
            if (logger.isDebugEnabled())
            {
                logger.debug("Defaulting on materializeAssocQName due to error.",
                             e);
            }
            return assocQName;
        }
    }
    private String handleExistingFile(NodeRef parentNodeRef, String fileName)
    {
        NodeServiceTrait actualNodeService = getTrait();
        NodeRef existingFile = actualNodeService.getChildByName(parentNodeRef,
                                                                ContentModel.ASSOC_CONTAINS,
                                                                fileName);
        if (existingFile != null)
        {
            int counter = 1;
            int dotIndex;
            String tmpFilename = "";
            final String dot = ".";
            final String hyphen = "-";
            while (existingFile != null)
            {
                int beforeCounter = fileName.lastIndexOf(hyphen);
                dotIndex = fileName.lastIndexOf(dot);
                if (dotIndex == 0)
                {
                    // File didn't have a proper 'name' instead it had just a
                    // suffix and
                    // started with a ".", create "1.txt"
                    tmpFilename = counter + fileName;
                }
                else if (dotIndex > 0)
                {
                    if (beforeCounter > 0 && beforeCounter < dotIndex)
                    {
                        // does file have counter in it's name or it just
                        // contains -1
                        String originalFileName = fileName.substring(0,
                                                                     beforeCounter)
                                    + fileName.substring(dotIndex);
                        boolean doesOriginalFileExist = actualNodeService.getChildByName(parentNodeRef,
                                                                                         ContentModel.ASSOC_CONTAINS,
                                                                                         originalFileName) != null;
                        if (doesOriginalFileExist)
                        {
                            String counterStr = fileName.substring(beforeCounter + 1,
                                                                   dotIndex);
                            try
                            {
                                int parseInt = DefaultTypeConverter.INSTANCE.intValue(counterStr);
                                counter = parseInt + 1;
                                fileName = fileName.substring(0,
                                                              beforeCounter)
                                            + fileName.substring(dotIndex);
                                dotIndex = fileName.lastIndexOf(dot);
                            }
                            catch (NumberFormatException ex)
                            {
                                // "-" is not before counter
                            }
                        }
                    }
                    tmpFilename = fileName.substring(0,
                                                     dotIndex)
                                + hyphen + counter + fileName.substring(dotIndex);
                }
                else
                {
                    // Filename didn't contain a dot at all, create "filename-1"
                    tmpFilename = fileName + hyphen + counter;
                }
                existingFile = actualNodeService.getChildByName(parentNodeRef,
                                                                ContentModel.ASSOC_CONTAINS,
                                                                tmpFilename);
                counter++;
            }
            fileName = tmpFilename;
        }
        return fileName;
    }
    @Override
    public List getParentAssocs(NodeRef nodeRef)
    {
        if (Reference.isReference(nodeRef))
        {
            return getParentAssocs(nodeRef,
                                   RegexQNamePattern.MATCH_ALL,
                                   RegexQNamePattern.MATCH_ALL);
        }
        else
        {
            return getTrait().getParentAssocs(nodeRef);
        }
    }
    @Override
    public List getParentAssocs(NodeRef nodeRef, QNamePattern typeQNamePattern,
                QNamePattern qnamePattern)
    {
        NodeServiceTrait theTrait = getTrait();
        if (Reference.isReference(nodeRef))
        {
            Reference reference = Reference.fromNodeRef(nodeRef);
            Reference parent = reference.execute(new GetParentReferenceMethod());
            if (parent == null)
            {
                return theTrait.getParentAssocs(reference.execute(new GetActualNodeRefMethod(environment)),
                                                typeQNamePattern,
                                                qnamePattern);
            }
            else
            {
                if (typeQNamePattern.isMatch(ContentModel.ASSOC_CONTAINS))
                {
                    Reference parentsParent = parent.execute(new GetParentReferenceMethod());
                    NodeRef parentNodeRef = parent.toNodeRef();
                    if (parentsParent == null)
                    {
                        parentNodeRef = parent.execute(new GetActualNodeRefMethod(environment));
                    }
                    NodeRef referenceNodeRef = reference.toNodeRef();
                    Map properties = smartStore.getProperties(reference);
                    Serializable name = properties.get(ContentModel.PROP_NAME);
                    QName assocChildName = QName
                                .createQNameWithValidLocalName(VirtualContentModel.VIRTUAL_CONTENT_MODEL_1_0_URI,
                                                               name.toString());
                    if (qnamePattern.isMatch(assocChildName))
                    {
                        ChildAssociationRef assoc = new ChildAssociationRef(ContentModel.ASSOC_CONTAINS,
                                                                            parentNodeRef,
                                                                            assocChildName,
                                                                            referenceNodeRef);
                        return Arrays.asList(assoc);
                    }
                    else
                    {
                        return Collections.emptyList();
                    }
                }
                else
                {
                    return Collections.emptyList();
                }
            }
        }
        else
        {
            return theTrait.getParentAssocs(nodeRef,
                                            typeQNamePattern,
                                            qnamePattern);
        }
    }
    @Override
    public List getChildAssocs(NodeRef nodeRef)
    {
        NodeServiceTrait theTrait = getTrait();
        boolean canVirtualize = canVirtualizeAssocNodeRef(nodeRef);
        if (canVirtualize)
        {
            Reference reference = smartStore.virtualize(nodeRef);
            List virtualAssociations = smartStore.getChildAssocs(reference,
                                                                                        RegexQNamePattern.MATCH_ALL,
                                                                                        RegexQNamePattern.MATCH_ALL,
                                                                                        Integer.MAX_VALUE,
                                                                                        false);
            List associations = new LinkedList<>(virtualAssociations);
            if (smartStore.canMaterialize(reference))
            {
                NodeRef materialReference = smartStore.materialize(reference);
                List actualAssociations = theTrait.getChildAssocs(materialReference,
                                                                                       RegexQNamePattern.MATCH_ALL,
                                                                                       RegexQNamePattern.MATCH_ALL,
                                                                                       Integer.MAX_VALUE,
                                                                                       false);
                associations.addAll(actualAssociations);
            }
            return associations;
        }
        else
        {
            return theTrait.getChildAssocs(nodeRef);
        }
    }
    @Override
    public List getChildAssocs(NodeRef nodeRef, QNamePattern typeQNamePattern,
                QNamePattern qnamePattern)
    {
        NodeServiceTrait theTrait = getTrait();
        boolean canVirtualize = canVirtualizeAssocNodeRef(nodeRef);
        if (canVirtualize)
        {
            Reference reference = smartStore.virtualize(nodeRef);
            List virtualAssociations = smartStore.getChildAssocs(reference,
                                                                                        typeQNamePattern,
                                                                                        qnamePattern,
                                                                                        Integer.MAX_VALUE,
                                                                                        false);
            List associations = new LinkedList<>(virtualAssociations);
            if (smartStore.canMaterialize(reference))
            {
                NodeRef materialReference = smartStore.materialize(reference);
                List actualAssociations = theTrait.getChildAssocs(materialReference,
                                                                                       typeQNamePattern,
                                                                                       qnamePattern);
                associations.addAll(actualAssociations);
            }
            return associations;
        }
        else
        {
            return theTrait.getChildAssocs(nodeRef,
                                           typeQNamePattern,
                                           qnamePattern);
        }
    }
    @Override
    public List getChildAssocs(NodeRef nodeRef, QNamePattern typeQNamePattern,
                QNamePattern qnamePattern, int maxResults, boolean preload)
    {
        NodeServiceTrait theTrait = getTrait();
        boolean canVirtualize = canVirtualizeAssocNodeRef(nodeRef);
        if (canVirtualize)
        {
            Reference reference = smartStore.virtualize(nodeRef);
            List virtualAssociations = smartStore.getChildAssocs(reference,
                                                                                        typeQNamePattern,
                                                                                        qnamePattern,
                                                                                        maxResults,
                                                                                        preload);
            List associations = new LinkedList<>(virtualAssociations);
            if (associations.size() < maxResults)
            {
                if (smartStore.canMaterialize(reference))
                {
                    NodeRef materialReference = smartStore.materialize(reference);
                    List actualAssociations = theTrait.getChildAssocs(materialReference,
                                                                                           typeQNamePattern,
                                                                                           qnamePattern,
                                                                                           maxResults - associations
                                                                                                       .size(),
                                                                                           preload);
                    associations.addAll(actualAssociations);
                }
            }
            return associations;
        }
        else
        {
            return theTrait.getChildAssocs(nodeRef,
                                           typeQNamePattern,
                                           qnamePattern,
                                           maxResults,
                                           preload);
        }
    }
    private boolean canVirtualizeAssocNodeRef(NodeRef nodeRef)
    {
        boolean canVirtualize = nodeRef.getId().contains("solr") ? false : smartStore.canVirtualize(nodeRef);
        return canVirtualize;
    }
    @Override
    public List getChildAssocs(NodeRef nodeRef, QNamePattern typeQNamePattern,
                QNamePattern qnamePattern, boolean preload)
    {
        NodeServiceTrait theTrait = getTrait();
        boolean canVirtualize = canVirtualizeAssocNodeRef(nodeRef);
        if (canVirtualize)
        {
            Reference reference = smartStore.virtualize(nodeRef);
            List virtualAssociations = smartStore.getChildAssocs(reference,
                                                                                        typeQNamePattern,
                                                                                        qnamePattern,
                                                                                        Integer.MAX_VALUE,
                                                                                        preload);
            List associations = new LinkedList<>(virtualAssociations);
            if (smartStore.canMaterialize(reference))
            {
                NodeRef materialReference = smartStore.materialize(reference);
                List actualAssociations = theTrait.getChildAssocs(materialReference,
                                                                                       typeQNamePattern,
                                                                                       qnamePattern,
                                                                                       preload);
                associations.addAll(actualAssociations);
            }
            return associations;
        }
        else
        {
            return theTrait.getChildAssocs(nodeRef,
                                           typeQNamePattern,
                                           qnamePattern,
                                           preload);
        }
    }
    @Override
    public List getChildAssocs(NodeRef nodeRef, Set childNodeTypeQNames)
    {
        NodeServiceTrait theTrait = getTrait();
        boolean canVirtualize = canVirtualizeAssocNodeRef(nodeRef);
        if (canVirtualize)
        {
            Reference reference = smartStore.virtualize(nodeRef);
            List virtualAssociations = smartStore.getChildAssocs(reference,
                                                                                        childNodeTypeQNames);
            List associations = new LinkedList<>(virtualAssociations);
            if (smartStore.canMaterialize(reference))
            {
                NodeRef materialReference = smartStore.materialize(reference);
                List actualAssociations = theTrait.getChildAssocs(materialReference,
                                                                                       childNodeTypeQNames);
                associations.addAll(actualAssociations);
            }
            return associations;
        }
        else
        {
            return theTrait.getChildAssocs(nodeRef,
                                           childNodeTypeQNames);
        }
    }
    @Override
    public List getChildAssocsByPropertyValue(NodeRef nodeRef, QName propertyQName,
                Serializable value)
    {
        NodeServiceTrait theTrait = getTrait();
        boolean canVirtualize = canVirtualizeAssocNodeRef(nodeRef);
        if (canVirtualize)
        {
            Reference reference = smartStore.virtualize(nodeRef);
            List virtualAssociations = smartStore.getChildAssocsByPropertyValue(reference,
                                                                                                       propertyQName,
                                                                                                       value);
            List associations = new LinkedList<>(virtualAssociations);
            if (smartStore.canMaterialize(reference))
            {
                NodeRef materialReference = smartStore.materialize(reference);
                List actualAssociations = theTrait.getChildAssocsByPropertyValue(materialReference,
                                                                                                      propertyQName,
                                                                                                      value);
                associations.addAll(actualAssociations);
            }
            return associations;
        }
        else
        {
            return theTrait.getChildAssocsByPropertyValue(nodeRef,
                                                          propertyQName,
                                                          value);
        }
    }
    @Override
    public NodeRef getChildByName(NodeRef nodeRef, QName assocTypeQName, String childName)
    {
        // TODO: optimize
        if (smartStore.canVirtualize(nodeRef))
        {
            Reference virtualNode = smartStore.virtualize(nodeRef);
            Reference theChild = smartStore.getChildByName(virtualNode,
                                                             assocTypeQName,
                                                             childName);
            if (theChild != null)
            {
                NodeRef childNodeRef = theChild.toNodeRef();
                return childNodeRef;
            }
            if (smartStore.isVirtual(nodeRef))
            {
                return null;
            }
        }
        // TODO: add virtualizable enabler
        nodeRef = materializeIfPossible(nodeRef);
        return getTrait().getChildByName(nodeRef,
                                         assocTypeQName,
                                         childName);
    }
    @Override
    public ChildAssociationRef getPrimaryParent(NodeRef nodeRef)
    {
        if (Reference.isReference(nodeRef))
        {
            Reference reference = Reference.fromNodeRef(nodeRef);
            Reference parent = reference.execute(new GetParentReferenceMethod());
            if (parent == null)
            {
                return getTrait().getPrimaryParent(reference.execute(new GetActualNodeRefMethod(environment)));
            }
            else
            {
                Reference parentsParent = parent.execute(new GetParentReferenceMethod());
                NodeRef parentNodeRef = parent.toNodeRef();
                if (parentsParent == null)
                {
                    parentNodeRef = parent.execute(new GetActualNodeRefMethod(environment));
                }
                NodeRef referenceNodeRef = reference.toNodeRef();
                Map refProperties = smartStore.getProperties(reference);
                Serializable childName = refProperties.get(ContentModel.PROP_NAME);
                QName childAssocQName = QName
                            .createQNameWithValidLocalName(VirtualContentModel.VIRTUAL_CONTENT_MODEL_1_0_URI,
                                                           childName.toString());
                ChildAssociationRef assoc = new ChildAssociationRef(ContentModel.ASSOC_CONTAINS,
                                                                    parentNodeRef,
                                                                    childAssocQName,
                                                                    referenceNodeRef,
                                                                    true,
                                                                    -1);
                return assoc;
            }
        }
        else
        {
            return getTrait().getPrimaryParent(nodeRef);
        }
    }
    public void setDownloadAssociationsFolder(NodeRefExpression downloadAssocaiationsFolder)
    {
        this.downloadAssociationsFolder = downloadAssocaiationsFolder;
    }
    private void cleanUpDownloadTargetAssocs(NodeRef sourceNodeRef)
    {
        NodeRef tempFolderNodeRef = downloadAssociationsFolder.resolve();
        if (tempFolderNodeRef == null)
        {
            return;
        }
        String tempFileName = sourceNodeRef.getId();
        NodeRef tempFileNodeRef = environment.getChildByName(tempFolderNodeRef,
                                                             ContentModel.ASSOC_CONTAINS,
                                                             tempFileName);
        if (tempFileNodeRef == null)
        {
            return;
        }
        environment.delete(tempFileNodeRef);
    }
    private List getDownloadTargetAssocs(NodeRef sourceNodeRef)
    {
        try
        {
            List result = new ArrayList();
            NodeRef tempFolderNodeRef = downloadAssociationsFolder.resolve();
            if (tempFolderNodeRef == null)
            {
                return result;
            }
            String tempFileName = sourceNodeRef.getId();
            NodeRef tempFileNodeRef = environment.getChildByName(tempFolderNodeRef,
                                                                 ContentModel.ASSOC_CONTAINS,
                                                                 tempFileName);
            if (tempFileNodeRef == null)
            {
                return result;
            }
            List readLines = IOUtils.readLines(environment.openContentStream(tempFileNodeRef),
                                                       StandardCharsets.UTF_8);
            for (String line : readLines)
            {
                NodeRef targetRef = new NodeRef(line);
                AssociationRef assocRef = new AssociationRef(sourceNodeRef,
                                                             DownloadModel.ASSOC_REQUESTED_NODES,
                                                             targetRef);
                result.add(assocRef);
            }
            return result;
        }
        catch (IOException e)
        {
            throw new VirtualizationException(e);
        }
    }
    private void createDownloadAssociation(NodeRef sourceNodeRef, NodeRef targetRef)
    {
        NodeRef tempFolderNodeRef = downloadAssociationsFolder.resolve(true);
        String tempFileName = sourceNodeRef.getId();
        NodeRef tempFileNodeRef = environment.getChildByName(tempFolderNodeRef,
                                                             ContentModel.ASSOC_CONTAINS,
                                                             tempFileName);
        if (tempFileNodeRef == null)
        {
            FileInfo newTempFileInfo = environment.create(tempFolderNodeRef,
                                                          tempFileName,
                                                          ContentModel.TYPE_CONTENT);
            tempFileNodeRef = newTempFileInfo.getNodeRef();
            ContentWriter writer = environment.getWriter(tempFileNodeRef,
                                                         ContentModel.PROP_CONTENT,
                                                         true);
            writer.setMimetype("text/plain");
            writer.putContent(targetRef.toString());
        }
        else
        {
            ContentWriter writer = environment.getWriter(tempFileNodeRef,
                                                         ContentModel.PROP_CONTENT,
                                                         true);
            try
            {
                List readLines = IOUtils.readLines(environment.openContentStream(tempFileNodeRef),
                                                           StandardCharsets.UTF_8);
                String targetRefString = targetRef.toString();
                if (!readLines.contains(targetRefString))
                {
                    readLines.add(targetRefString);
                }
                String text = "";
                for (String line : readLines)
                {
                    if (text.isEmpty())
                    {
                        text = line;
                    }
                    else
                    {
                        text = text + IOUtils.LINE_SEPARATOR + line;
                    }
                }
                writer.putContent(text);
            }
            catch (IOException e)
            {
                throw new ActualEnvironmentException(e);
            }
        }
    }
    @Override
    public List getTargetAssocs(NodeRef sourceRef, QNamePattern qnamePattern)
    {
        NodeServiceTrait theTrait = getTrait();
        List targetAssocs = null;
        if (Reference.isReference(sourceRef))
        {
            Reference reference = Reference.fromNodeRef(sourceRef);
            if (smartStore.canMaterialize(reference))
            {
                NodeRef materializedReferece = smartStore.materialize(reference);
                targetAssocs = theTrait.getTargetAssocs(materializedReferece,
                                                        qnamePattern);
            }
            else
            {
                targetAssocs = new LinkedList<>();
            }
        }
        else
        {
            targetAssocs = theTrait.getTargetAssocs(sourceRef,
                                                    qnamePattern);
        }
        List virtualizedIfNeededTargetAssocs = null;
        if (Reference.isReference(sourceRef))
        {
            virtualizedIfNeededTargetAssocs = new LinkedList<>();
            Reference sourceReference = Reference.fromNodeRef(sourceRef);
            for (AssociationRef associationRef : targetAssocs)
            {
                NodeRef targetNodeRef = associationRef.getTargetRef();
                Reference targetReference = NodeProtocol.newReference(targetNodeRef,
                                                                      sourceReference
                                                                                  .execute(new GetParentReferenceMethod()));
                AssociationRef virtualAssocRef = new AssociationRef(associationRef.getId(),
                                                                    sourceRef,
                                                                    associationRef.getTypeQName(),
                                                                    targetReference.toNodeRef(targetNodeRef
                                                                                .getStoreRef()));
                virtualizedIfNeededTargetAssocs.add(virtualAssocRef);
            }
        }
        else
        {
            virtualizedIfNeededTargetAssocs = targetAssocs;
            if (DownloadModel.TYPE_DOWNLOAD.equals(environment.getType(sourceRef)))
            {
                if (qnamePattern.isMatch(DownloadModel.ASSOC_REQUESTED_NODES))
                {
                    List virtualTargetAssocs = getDownloadTargetAssocs(sourceRef);
                    virtualizedIfNeededTargetAssocs.addAll(virtualTargetAssocs);
                }
            }
        }
        return virtualizedIfNeededTargetAssocs;
    }
    @Override
    public List getSourceAssocs(NodeRef targetRef, QNamePattern qnamePattern)
    {
        NodeServiceTrait theTrait = getTrait();
        if (Reference.isReference(targetRef))
        {
            Reference reference = Reference.fromNodeRef(targetRef);
            List materialAssocs = new ArrayList();
            if (smartStore.canMaterialize(reference))
            {
                List sourceAssocs = theTrait.getSourceAssocs(smartStore.materialize(reference),
                                                                             qnamePattern);
                for (AssociationRef associationRef : sourceAssocs)
                {
                    NodeRef sourceRef = associationRef.getSourceRef();
                    Reference sourceReference = NodeProtocol.newReference(sourceRef,
                                                                          reference
                                                                                      .execute(new GetParentReferenceMethod()));
                    AssociationRef virtualAssocRef = new AssociationRef(associationRef.getId(),
                                                                        sourceReference.toNodeRef(),
                                                                        associationRef.getTypeQName(),
                                                                        targetRef);
                    materialAssocs.add(virtualAssocRef);
                }
            }
            // Download sources are deliberately not virtualized due to
            // performance and complexity issues.
            // However they could be detected using
            // if (qnamePattern.isMatch(DownloadModel.ASSOC_REQUESTED_NODES))
            return materialAssocs;
        }
        else
        {
            return theTrait.getSourceAssocs(targetRef,
                                            qnamePattern);
        }
    }
    @Override
    public ChildAssociationRef moveNode(NodeRef nodeToMoveRef, NodeRef newParentRef, QName assocTypeQName,
                QName assocQName)
    {
        if (Reference.isReference(nodeToMoveRef) || Reference.isReference(newParentRef))
        {
            throw new UnsupportedOperationException("Unsuported operation for virtual source or destination");
        }
        else
        {
            return getTrait().moveNode(nodeToMoveRef,
                                       newParentRef,
                                       assocTypeQName,
                                       assocQName);
        }
    }
    @Override
    public void addProperties(NodeRef nodeRef, Map properties)
    {
        if (Reference.isReference(nodeRef))
        {
            Reference reference = Reference.fromNodeRef(nodeRef);
            NodeRef actualNodeRef = reference.execute(new GetActualNodeRefMethod(null));
            getTrait().addProperties(actualNodeRef,
                                     properties);
        }
        else
        {
            getTrait().addProperties(nodeRef,
                                     properties);
        }
    }
    @Override
    public AssociationRef createAssociation(NodeRef sourceRef, NodeRef targetRef, QName assocTypeQName)
    {
        if (Reference.isReference(targetRef)
                    && getTrait().getType(materializeIfPossible(sourceRef)).equals(DownloadModel.TYPE_DOWNLOAD))
        {
            // NOTE : this is enables downloads of virtual structures
            createDownloadAssociation(sourceRef,
                                      targetRef);
            AssociationRef assocRef = new AssociationRef(sourceRef,
                                                         assocTypeQName,
                                                         targetRef);
            return assocRef;
        }
        else
        {
            return getTrait().createAssociation(materializeIfPossible(sourceRef),
                                                materializeIfPossible(targetRef),
                                                assocTypeQName);
        }
    }
    private List materializeIfPossible(Collection nodeRefs)
    {
        List nodeRefList = new LinkedList<>();
        for (NodeRef nodeRef : nodeRefs)
        {
            nodeRefList.add(materializeIfPossible(nodeRef));
        }
        return nodeRefList;
    }
    /**
     * @deprecated use {@link VirtualStore#materializeIfPossible(NodeRef)}
     *             instead
     */
    private NodeRef materializeIfPossible(NodeRef nodeRef)
    {
        if (Reference.isReference(nodeRef))
        {
            Reference ref = Reference.fromNodeRef(nodeRef);
            if (smartStore.canMaterialize(ref))
            {
                return smartStore.materialize(ref);
            }
        }
        return nodeRef;
    }
    @Override
    public List getStores()
    {
        return getTrait().getStores();
    }
    @Override
    public StoreRef createStore(String protocol, String identifier) throws StoreExistsException
    {
        return getTrait().createStore(protocol,
                                      identifier);
    }
    @Override
    public void deleteStore(StoreRef storeRef)
    {
        getTrait().deleteStore(storeRef);
    }
    @Override
    public boolean exists(StoreRef storeRef)
    {
        return getTrait().exists(storeRef);
    }
    @Override
    public Status getNodeStatus(NodeRef nodeRef)
    {
        return getTrait().getNodeStatus(materializeIfPossible(nodeRef));
    }
    @Override
    public NodeRef getNodeRef(Long nodeId)
    {
        return getTrait().getNodeRef(nodeId);
    }
    @Override
    public NodeRef getRootNode(StoreRef storeRef) throws InvalidStoreRefException
    {
        return getTrait().getRootNode(storeRef);
    }
    @Override
    public Set getAllRootNodes(StoreRef storeRef)
    {
        return getTrait().getAllRootNodes(storeRef);
    }
    @Override
    public void setChildAssociationIndex(ChildAssociationRef childAssocRef, int index)
                throws InvalidChildAssociationRefException
    {
        getTrait().setChildAssociationIndex(childAssocRef,
                                            index);
    }
    @Override
    public void setType(NodeRef nodeRef, QName typeQName) throws InvalidNodeRefException
    {
        getTrait().setType(materializeIfPossible(nodeRef),
                           typeQName);
    }
    @Override
    public void addAspect(NodeRef nodeRef, QName aspectTypeQName, Map aspectProperties)
                throws InvalidNodeRefException, InvalidAspectException
    {
        getTrait().addAspect(materializeIfPossible(nodeRef),
                             aspectTypeQName,
                             aspectProperties);
    }
    @Override
    public void removeAspect(NodeRef nodeRef, QName aspectTypeQName)
                throws InvalidNodeRefException, InvalidAspectException
    {
        getTrait().removeAspect(materializeIfPossible(nodeRef),
                                aspectTypeQName);
    }
    @Override
    public void deleteNode(NodeRef nodeRef) throws InvalidNodeRefException
    {
        NodeServiceTrait theTrait = getTrait();
        NodeRef materialNode = smartStore.materializeIfPossible(nodeRef);
        boolean isDownload = DownloadModel.TYPE_DOWNLOAD.equals(theTrait.getType(materialNode));
        theTrait.deleteNode(materialNode);
        if (isDownload)
        {
            cleanUpDownloadTargetAssocs(nodeRef);
        }
    }
    @Override
    public ChildAssociationRef addChild(NodeRef parentRef, NodeRef childRef, QName assocTypeQName, QName qname)
                throws InvalidNodeRefException
    {
        return getTrait().addChild(materializeIfPossible(parentRef),
                                   materializeIfPossible(childRef),
                                   assocTypeQName,
                                   qname);
    }
    @Override
    public List addChild(Collection parentRefs, NodeRef childRef, QName assocTypeQName,
                QName qname) throws InvalidNodeRefException
    {
        return getTrait().addChild(materializeIfPossible(parentRefs),
                                   materializeIfPossible(childRef),
                                   assocTypeQName,
                                   qname);
    }
    @Override
    public void removeChild(NodeRef parentRef, NodeRef childRef) throws InvalidNodeRefException
    {
        getTrait().removeChild(materializeIfPossible(parentRef),
                               materializeIfPossible(childRef));
    }
    @Override
    public boolean removeChildAssociation(ChildAssociationRef childAssocRef)
    {
        NodeServiceTrait theTrait = getTrait();
        NodeRef childRef = childAssocRef.getChildRef();
        if (Reference.isReference(childRef))
        {
            List assocsToRemove = revertVirtualAssociation(childAssocRef,
                                                                                theTrait,
                                                                                childRef);
            boolean removed = false;
            if (!assocsToRemove.isEmpty())
            {
                for (ChildAssociationRef assoc : assocsToRemove)
                {
                    removed = removed || theTrait.removeChildAssociation(assoc);
                }
            }
            return removed;
        }
        else
        {
            return theTrait.removeChildAssociation(childAssocRef);
        }
    }
    private List revertVirtualAssociation(ChildAssociationRef childAssocRef,
                NodeServiceTrait theTrait, NodeRef childRef)
    {
        childRef = smartStore.materialize(Reference.fromNodeRef(childRef));
        ChildAssociationRef parent = theTrait.getPrimaryParent(childRef);
        final QName assocName = childAssocRef.getQName();
        List assocsToRemove = theTrait.getChildAssocs(parent.getParentRef(),
                                                                           childAssocRef.getTypeQName(),
                                                                           new QNamePattern()
                                                                           {
                                                                               @Override
                                                                               public boolean isMatch(QName qname)
                                                                               {
                                                                                   return assocName
                                                                                               .getLocalName()
                                                                                                   .equals(qname
                                                                                                               .getLocalName());
                                                                               }
                                                                           });
        return assocsToRemove;
    }
    @Override
    public boolean removeSeconaryChildAssociation(ChildAssociationRef childAssocRef)
    {
        NodeServiceTrait theTrait = getTrait();
        NodeRef childRef = childAssocRef.getChildRef();
        if (Reference.isReference(childRef))
        {
            List assocsToRemove = revertVirtualAssociation(childAssocRef,
                                                                                theTrait,
                                                                                childRef);
            boolean removed = false;
            if (!assocsToRemove.isEmpty())
            {
                for (ChildAssociationRef assoc : assocsToRemove)
                {
                    removed = removed || theTrait.removeSeconaryChildAssociation(assoc);
                }
            }
            return removed;
        }
        else
        {
            return theTrait.removeSeconaryChildAssociation(childAssocRef);
        }
    }
    @Override
    public boolean removeSecondaryChildAssociation(ChildAssociationRef childAssocRef)
    {
        NodeServiceTrait theTrait = getTrait();
        NodeRef childRef = childAssocRef.getChildRef();
        if (Reference.isReference(childRef))
        {
            List assocsToRemove = revertVirtualAssociation(childAssocRef,
                                                                                theTrait,
                                                                                childRef);
            boolean removed = false;
            if (!assocsToRemove.isEmpty())
            {
                for (ChildAssociationRef assoc : assocsToRemove)
                {
                    removed = removed || theTrait.removeSecondaryChildAssociation(assoc);
                }
            }
            return removed;
        }
        else
        {
            return theTrait.removeSecondaryChildAssociation(childAssocRef);
        }
    }
    @Override
    public Long getNodeAclId(NodeRef nodeRef) throws InvalidNodeRefException
    {
        return getTrait().getNodeAclId(materializeIfPossible(nodeRef));
    }
    @Override
    public void setProperties(NodeRef nodeRef, Map properties) throws InvalidNodeRefException
    {
        getTrait().setProperties(materializeIfPossible(nodeRef),
                                 properties);
    }
    @Override
    public void setProperty(NodeRef nodeRef, QName qname, Serializable value) throws InvalidNodeRefException
    {
        getTrait().setProperty(materializeIfPossible(nodeRef),
                               qname,
                               value);
    }
    @Override
    public void removeProperty(NodeRef nodeRef, QName qname) throws InvalidNodeRefException
    {
        getTrait().removeProperty(materializeIfPossible(nodeRef),
                                  qname);
    }
    @Override
    public List getChildrenByName(NodeRef nodeRef, QName assocTypeQName,
                Collection childNames)
    {
        return getTrait().getChildrenByName(materializeIfPossible(nodeRef),
                                            assocTypeQName,
                                            childNames);
    }
    @Override
    public Collection getChildAssocsWithoutParentAssocsOfType(NodeRef nodeRef,
                QName assocTypeQName)
    {
        NodeServiceTrait theTrait = getTrait();
        boolean canVirtualize = canVirtualizeAssocNodeRef(nodeRef);
        if (canVirtualize)
        {
            Reference reference = smartStore.virtualize(nodeRef);
            Collection virtualAssociations = smartStore
                        .getChildAssocsWithoutParentAssocsOfType(reference,
                                                                 assocTypeQName);
            List associations = new LinkedList<>(virtualAssociations);
            if (smartStore.canMaterialize(reference))
            {
                NodeRef materialReference = smartStore.materialize(reference);
                Collection actualAssociations = theTrait
                            .getChildAssocsWithoutParentAssocsOfType(materialReference,
                                                                     assocTypeQName);
                associations.addAll(actualAssociations);
            }
            return associations;
        }
        else
        {
            return theTrait.getChildAssocsWithoutParentAssocsOfType(nodeRef,
                                                                    assocTypeQName);
        }
    }
    @Override
    public void removeAssociation(NodeRef sourceRef, NodeRef targetRef, QName assocTypeQName)
                throws InvalidNodeRefException
    {
        getTrait().removeAssociation(materializeIfPossible(sourceRef),
                                     materializeIfPossible(targetRef),
                                     assocTypeQName);
    }
    @Override
    public void setAssociations(NodeRef sourceRef, QName assocTypeQName, List targetRefs)
    {
        getTrait().setAssociations(materializeIfPossible(sourceRef),
                                   assocTypeQName,
                                   materializeIfPossible(targetRefs));
    }
    @Override
    public AssociationRef getAssoc(Long id)
    {
        return getTrait().getAssoc(id);
    }
    @Override
    public NodeRef getStoreArchiveNode(StoreRef storeRef)
    {
        return getTrait().getStoreArchiveNode(storeRef);
    }
    @Override
    public NodeRef restoreNode(NodeRef archivedNodeRef, NodeRef destinationParentNodeRef, QName assocTypeQName,
                QName assocQName)
    {
        return getTrait().restoreNode(materializeIfPossible(archivedNodeRef),
                                      materializeIfPossible(destinationParentNodeRef),
                                      assocTypeQName,
                                      assocQName);
    }
    @Override
    public List findNodes(FindNodeParameters params)
    {
        return getTrait().findNodes(params);
    }
    @Override
    public int countChildAssocs(NodeRef nodeRef, boolean isPrimary) throws InvalidNodeRefException
    {
        return getTrait().countChildAssocs(nodeRef,
                                           isPrimary);
    }
    @Override
    public List getTargetAssocsByPropertyValue(NodeRef sourceRef, QNamePattern qnamePattern,
                QName propertyQName, Serializable propertyValue)
    {
        return getTrait().getTargetAssocsByPropertyValue(sourceRef,
                                                         qnamePattern,
                                                         propertyQName,
                                                         propertyValue);
    }
}