org.alfresco.repo.transfer.fsr.ManifestProcessorImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.repo.transfer.fsr.ManifestProcessorImpl.java

Source

/*
 * #%L
 * Alfresco File Transfer Receiver
 * %%
 * 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.transfer.fsr;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.transfer.AbstractManifestProcessorBase;
import org.alfresco.repo.transfer.TransferCommons;
import org.alfresco.repo.transfer.TransferFatalException;
import org.alfresco.repo.transfer.TransferProcessingException;
import org.alfresco.repo.transfer.TransferProgressMonitor;
import org.alfresco.repo.transfer.manifest.TransferManifestDeletedNode;
import org.alfresco.repo.transfer.manifest.TransferManifestHeader;
import org.alfresco.repo.transfer.manifest.TransferManifestNode;
import org.alfresco.repo.transfer.manifest.TransferManifestNormalNode;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.transfer.TransferReceiver;
import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class ManifestProcessorImpl extends AbstractManifestProcessorBase {
    private static final String MSG_FAILED_TO_CREATE_FOLDER = "ftr.failedToDeleteFolder";

    private static final String MSG_FAILED_TO_DELETE_FILE = "ftr.failedToDeleteFile";

    private static final String MSG_ERROR_COPYING_FILE = "ftr.errorCopyingFile";

    private Log log = LogFactory.getLog(ManifestProcessorImpl.class);

    private Map<String, List<NodeContext>> orphans = new TreeMap<String, List<NodeContext>>();
    private Map<String, NodeContext> foldersToDelete = new TreeMap<String, NodeContext>();
    private Map<String, NodeContext> tempFilesToRename = new TreeMap<String, NodeContext>();
    private Map<String, NodeContext> existingFilesToReplace = new TreeMap<String, NodeContext>();
    private Map<String, NodeContext> foldersToMove = new TreeMap<String, NodeContext>();
    private Set<String> receivedFolderIds = new TreeSet<String>();
    private Set<String> receivedFileIds = new TreeSet<String>();
    private Map<String, Set<String>> parentChildMap = new TreeMap<String, Set<String>>();
    private DbHelper dbHelper;
    private final boolean isDebugEnabled;

    private long processStartTime;

    private FileTransferReceiver fileTransferReceiver;

    private boolean isSync;

    public ManifestProcessorImpl(TransferReceiver receiver, String transferId, DbHelper dbHelper) {
        super(receiver, transferId);
        this.fileTransferReceiver = (FileTransferReceiver) receiver;
        this.dbHelper = dbHelper;
        this.isDebugEnabled = log.isDebugEnabled();
    }

    @Override
    protected void processHeader(TransferManifestHeader header) {
        this.isSync = header.isSync();
    }

    @Override
    protected void startManifest() {
        processStartTime = System.currentTimeMillis();
        NodeContext.renamingCounter = 1;
        //Make sure we have the root node recorded...
        //The root node is the only one that has "" as its parent id

        String rootNodeId = fileTransferReceiver.getTransferRootNode();
        String rootFolderLocation = fileTransferReceiver.getDefaultReceivingroot();
        if (isDebugEnabled) {
            log.debug("Checking that root node and corresponding folder exist: " + rootNodeId + "   "
                    + rootFolderLocation);
        }
        FileTransferInfoEntity rootNodeEntity = dbHelper.findFileTransferInfoByNodeRef(rootNodeId);
        if (rootNodeEntity == null) {
            if (isDebugEnabled) {
                log.debug("Failed to find root node in database. Creating...");
            }
            File rootFolder = new File(rootFolderLocation);
            boolean rootFolderExists = rootFolder.exists();
            if (!rootFolderExists) {
                if (isDebugEnabled) {
                    log.debug("Root folder does not exist. Creating...");
                }
                rootFolderExists = rootFolder.mkdir();
            }
            if (rootFolderExists) {
                if (isDebugEnabled) {
                    log.debug("Root folder exists. Creating node record in database.");
                }
                dbHelper.createNodeInDB(rootNodeId, "", "", "", "", true);
            }
        } else {
            if (isDebugEnabled) {
                log.debug("Root node already exists in the database.");
            }
        }
    }

    @Override
    protected void endManifest() {
        String pathPrefix = fileTransferReceiver.getDefaultReceivingroot();

        if (isDebugEnabled) {
            log.debug("Initial pass through manifest is complete. Post-processing has started.");
        }

        //Process any existing files that need to be replaced with new versions
        //Copy the collection first so we can safely remove processed files as we go. This helps
        //us later if we ever need to clean up following an error.
        Collection<NodeContext> filesToReplace = new ArrayList<NodeContext>(existingFilesToReplace.values());
        for (NodeContext fileToReplace : filesToReplace) {
            if (switchFile(fileToReplace.nodeId, fileToReplace.newParentId, fileToReplace.tempName,
                    fileToReplace.newContentUrl, pathPrefix)) {
                //Record this node in the list of temp files to be renamed
                tempFilesToRename.put(fileToReplace.nodeId, fileToReplace);
                existingFilesToReplace.remove(fileToReplace.nodeId);
            }
        }

        //Deal with any folders that need moving
        int folderCount;
        Set<String> processedFolders = new TreeSet<String>();
        while (!foldersToMove.isEmpty()) {
            folderCount = foldersToMove.size();
            if (isDebugEnabled) {
                log.debug("Folders that need to be moved: " + folderCount);
            }
            for (NodeContext folder : foldersToMove.values()) {
                FileTransferInfoEntity folderEntity = dbHelper.findFileTransferInfoByNodeRef(folder.nodeId);
                FileTransferInfoEntity parentEntity = dbHelper.findFileTransferInfoByNodeRef(folder.newParentId);
                if (moveFolder(folderEntity, parentEntity, folder.newName, pathPrefix)) {
                    processedFolders.add(folder.nodeId);

                    //Log the effect that this has had...
                    if (folder.isNew) {
                        logCreated(folder.nodeId, folder.newParentId,
                                pathPrefix + folderEntity.getPath() + folderEntity.getContentName(), false);
                    } else {
                        logMoved(folder.nodeId, pathPrefix + folder.currentParentPath + folder.currentName,
                                folder.newParentId,
                                pathPrefix + folderEntity.getPath() + folderEntity.getContentName());
                    }
                }
            }
            for (String nodeId : processedFolders) {
                foldersToMove.remove(nodeId);
            }
            processedFolders.clear();
            if (folderCount == foldersToMove.size()) {
                //We have reached a point where we have failed to process any more folders
                log.error("We failed to move any folders successfully on that loop.");
                break;
            }
        }

        //If any folders need to be deleted then handle them now
        removeDeletedFolders(pathPrefix);

        //If we are dealing with a "sync" transfer then we now need to work out
        //if there are any implicit deletions required and process them if so.
        if (isSync) {
            if (isDebugEnabled) {
                log.debug("Sync-mode transfer: started checking received data for implicit deletes...");
            }
            //For each folder that we received in the transfer, check which children we have received in this transfer and
            //compare with the list of children that we currently have. If there are any existing children that
            //we didn't receive in this transfer then we assume that we must delete them...
            for (String parentId : receivedFolderIds) {
                Set<String> receivedChildren = parentChildMap.get(parentId);
                List<FileTransferInfoEntity> currentChildren = dbHelper
                        .findFileTransferInfoByParentNodeRef(parentId);
                for (FileTransferInfoEntity currentChild : currentChildren) {
                    if (receivedChildren == null || !receivedChildren.remove(currentChild.getNodeRef())) {
                        if (isDebugEnabled) {
                            log.debug("Have not received data for existing node " + currentChild.getNodeRef() + " ("
                                    + currentChild.getPath() + currentChild.getContentName() + ")");
                        }
                        deleteNode(currentChild, pathPrefix);
                    }
                }
            }
            if (isDebugEnabled) {
                log.debug("Sync-mode transfer: finished checking received data for implicit deletes.");
            }
        }

        //Finally we need to run through all the new files with temporary names and rename them
        renameTempFiles(pathPrefix);

        log.info("Completed processing manifest file. It took " + (System.currentTimeMillis() - processStartTime)
                + "ms");
    }

    private void removeDeletedFolders(String pathPrefix) {
        Collection<NodeContext> folders = new ArrayList<NodeContext>(foldersToDelete.values());
        for (NodeContext folderToDelete : folders) {
            FileTransferInfoEntity folderEntity = dbHelper.findFileTransferInfoByNodeRef(folderToDelete.nodeId);
            if (deleteNode(folderEntity, pathPrefix)) {
                foldersToDelete.remove(folderToDelete.nodeId);
                logDeleted(folderToDelete.nodeId,
                        pathPrefix + folderToDelete.currentParentPath + folderToDelete.currentName);
            }
        }
    }

    //Delete the supplied node, going depth-first if it is a folder
    protected boolean deleteNode(FileTransferInfoEntity nodeToDelete, String pathPrefix) {
        boolean success = true;
        if (nodeToDelete != null) {
            String nodeId = nodeToDelete.getNodeRef();
            if (nodeToDelete.isFolder()) {
                List<FileTransferInfoEntity> childrenList = dbHelper.findFileTransferInfoByParentNodeRef(nodeId);
                List<FileTransferInfoEntity> files = new ArrayList<FileTransferInfoEntity>(childrenList.size());
                for (FileTransferInfoEntity child : childrenList) {
                    //Go depth first. Process folders first and files after....
                    if (child.isFolder()) {
                        success &= deleteNode(child, pathPrefix);
                        if (!success)
                            return false;
                    } else {
                        //Postpone processing of files
                        files.add(child);
                    }
                }
                //We've now finished deleting the child folders. Now we'll delete the child files... 
                for (FileTransferInfoEntity childFile : files) {
                    success &= deleteNode(childFile, pathPrefix);
                    if (!success)
                        return false;
                }
            }
            String path = pathPrefix + nodeToDelete.getPath() + nodeToDelete.getContentName();
            File fileToDelete = new File(path);
            if (isDebugEnabled) {
                log.debug("Attempting to delete file/folder " + path);
            }
            if (fileToDelete.delete()) {
                if (isDebugEnabled) {
                    log.debug("Successfully deleted file/folder. Updating database.");
                }
                dbHelper.deleteNodeByNodeRef(nodeId);
                logDeleted(nodeId, path);
            } else {
                success = false;
                log.error("Failed to delete file/folder.");
            }
        }
        return success;
    }

    private boolean moveFolder(FileTransferInfoEntity nodeEntity, FileTransferInfoEntity parentEntity,
            String newName, String pathPrefix) {
        boolean successful = false;
        if (nodeEntity != null && parentEntity != null) {
            String currentParentPath = nodeEntity.getPath();
            String currentFolderName = nodeEntity.getContentName();
            String targetParentPath = parentEntity.getPath() + parentEntity.getContentName() + "/";
            File srcFolder = new File(pathPrefix + currentParentPath + currentFolderName);
            File destFolder = new File(pathPrefix + targetParentPath + newName);
            if (isDebugEnabled) {
                log.debug("Attempting to move folder \"" + srcFolder.getPath() + "\" to " + destFolder.getPath()
                        + "\"");
            }
            successful = srcFolder.renameTo(destFolder);
            if (successful) {
                if (isDebugEnabled) {
                    log.debug("Successfully moved folder. Updating database.");
                }
                nodeEntity.setContentName(newName);
                nodeEntity.setPath(targetParentPath);
                nodeEntity.setParent(parentEntity.getNodeRef());
                dbHelper.updateFileTransferInfoByNodeRef(nodeEntity);
                dbHelper.updatePathOfChildren(nodeEntity.getNodeRef(), targetParentPath + newName + "/");
            } else {
                log.error(
                        "Failed to move folder \"" + srcFolder.getPath() + "\" to " + destFolder.getPath() + "\"");
            }
        }
        return successful;
    }

    private boolean switchFile(String nodeId, String newParentId, String targetFileName, String newContentUrl,
            String pathPrefix) {
        boolean successful = false;

        if (isDebugEnabled) {
            log.debug("Switching from old to new file for node " + nodeId);
        }

        //Update the database entry to point at the new file
        FileTransferInfoEntity nodeEntity = dbHelper.findFileTransferInfoByNodeRef(nodeId);
        FileTransferInfoEntity parentEntity = dbHelper.findFileTransferInfoByNodeRef(newParentId);
        if (nodeEntity != null && parentEntity != null) {
            String currentParentPath = nodeEntity.getPath();
            String currentFileName = nodeEntity.getContentName();
            String newParentPath = parentEntity.getPath() + parentEntity.getContentName() + "/";
            nodeEntity.setContentName(targetFileName);
            nodeEntity.setParent(newParentId);
            nodeEntity.setPath(newParentPath);
            nodeEntity.setContentUrl(newContentUrl);
            dbHelper.updateFileTransferInfoByNodeRef(nodeEntity);
            if (isDebugEnabled) {
                log.debug("Switched file from \"" + currentParentPath + currentFileName + "\" to \"" + newParentPath
                        + targetFileName + "\"");
                log.debug("Attempting to delete the original file");
            }

            //Delete the now-obsolete file
            String pathOfDeletedFile = pathPrefix + currentParentPath + currentFileName;
            File fileToDelete = new File(pathOfDeletedFile);
            successful = fileToDelete.delete();
            if (isDebugEnabled) {
                log.debug("Deletion of original file " + (successful ? "succeeded" : "FAILED"));
            }
        }
        return successful;
    }

    @Override
    protected void processNode(TransferManifestDeletedNode node) throws TransferProcessingException {
        TransferProgressMonitor monitor = fileTransferReceiver.getProgressMonitor();
        String nodeId = node.getNodeRef().toString();
        if (isDebugEnabled) {
            log.debug("Processing deleted node " + nodeId);
        }
        boolean nodeIsRoot = nodeId.equals(fileTransferReceiver.getTransferRootNode());
        if (nodeIsRoot) {
            monitor.logComment(getTransferId(), "We have received the root node. Skipping " + nodeId);
            return;
        }

        FileTransferInfoEntity nodeEntity = dbHelper.findFileTransferInfoByNodeRef(nodeId);
        if (nodeEntity != null) {
            NodeContext ctx = NodeContext.buildNodeContext(node, nodeEntity);
            String pathPrefix = fileTransferReceiver.getDefaultReceivingroot();
            String pathOfDeletedFile = pathPrefix + nodeEntity.getPath() + nodeEntity.getContentName();
            File fileToDelete = new File(pathOfDeletedFile);
            if (fileToDelete.isDirectory()) {
                if (isDebugEnabled) {
                    log.debug("Processing request to delete folder " + fileToDelete.getPath());
                }
                //Rename the folder to a temporary name to avoid any name conflicts that may occur later.
                FileTransferInfoEntity parentEntity = dbHelper
                        .findFileTransferInfoByNodeRef(nodeEntity.getParent());
                if (moveFolder(nodeEntity, parentEntity, ctx.tempName, pathPrefix)) {
                    if (isDebugEnabled) {
                        log.debug("Recording request to delete folder " + fileToDelete.getPath());
                    }
                    recordFolderDelete(ctx);
                }
            } else {
                if (isDebugEnabled) {
                    log.debug("Processing request to delete file " + fileToDelete.getPath());
                }
                boolean success = fileToDelete.delete();
                if (success) {
                    if (isDebugEnabled) {
                        log.debug("Successfully deleted " + fileToDelete.getPath());
                    }
                    dbHelper.deleteNodeByNodeRef(nodeId);
                    if (isDebugEnabled) {
                        log.debug("Updated database to reflect deletion.");
                    }
                    monitor.logDeleted(getTransferId(), node.getNodeRef(), node.getNodeRef(),
                            fileToDelete.getPath());
                } else {
                    throw new TransferFatalException(MSG_FAILED_TO_DELETE_FILE,
                            new Object[] { nodeId, pathOfDeletedFile });
                }
            }
        }

    }

    private void recordFolderDelete(NodeContext nodeCtx) {
        foldersToDelete.put(nodeCtx.nodeId, nodeCtx);
    }

    @Override
    protected void processNode(TransferManifestNormalNode node) throws TransferProcessingException {
        TransferProgressMonitor monitor = fileTransferReceiver.getProgressMonitor();

        String nodeId = node.getNodeRef().toString();
        boolean nodeIsRoot = nodeId.equals(fileTransferReceiver.getTransferRootNode());
        String newParentId = node.getPrimaryParentAssoc().getParentRef().toString();

        FileTransferInfoEntity nodeEntity = dbHelper.findFileTransferInfoByNodeRef(nodeId);
        if (nodeIsRoot) {
            receivedFolderIds.add(nodeId);
            monitor.logComment(getTransferId(), "We have received the root node. Skipping " + nodeId);
            return;
        }
        if (!ContentModel.ASSOC_CONTAINS.equals(node.getPrimaryParentAssoc().getTypeQName())
                || !(ContentModel.TYPE_FOLDER.equals(node.getAncestorType())
                        || ContentModel.TYPE_CONTENT.equals(node.getAncestorType()))) {
            monitor.logComment(getTransferId(),
                    "Skipping node due to either: not content; not folder; or not cm:contains: " + nodeId);
            return;
        }

        FileTransferInfoEntity parentEntity = dbHelper.findFileTransferInfoByNodeRef(newParentId);

        NodeContext ctx = NodeContext.buildNodeContext(node, nodeEntity, parentEntity);

        if (!ctx.parentAlreadyExists) {
            //This is an orphan. Record it as such and come back to it later
            if (isDebugEnabled) {
                log.debug("We have not received the parent folder yet for " + nodeId + " (" + ctx.newParentId
                        + "). Recording as an orphan.");
            }
            recordOrphan(ctx);
            return;
        }

        processContext(ctx);
    }

    private void processContext(NodeContext ctx) {
        if (isDebugEnabled) {
            log.debug("Processing received node " + ctx.nodeId + " (" + ctx.newName + ")");
        }

        String tempName = ctx.tempName;
        String pathPrefix = fileTransferReceiver.getDefaultReceivingroot();
        recordForSyncMode(ctx);
        if (ctx.isFolder) {
            if (isDebugEnabled) {
                log.debug("This node is a folder");
            }
            if (ctx.isNew) {
                if (isDebugEnabled) {
                    log.debug("This node has never been received before");
                }
                //Create a new folder in the correct place but with a temporary name
                File newFolder = new File(pathPrefix + ctx.newParentPath, tempName);
                if (isDebugEnabled) {
                    log.debug("Attempting to create a new folder: " + newFolder.getPath());
                }
                boolean success = newFolder.mkdir();
                if (!success) {
                    if (isDebugEnabled) {
                        log.debug("Failed to create a new folder: " + newFolder.getPath());
                    }
                    throw new TransferFatalException(MSG_FAILED_TO_CREATE_FOLDER,
                            new Object[] { newFolder.getPath() });
                }
                if (isDebugEnabled) {
                    log.debug("Successfully created a new folder: " + newFolder.getPath());
                    log.debug("Recording in database");
                }
                //Store in the database
                dbHelper.createNodeInDB(ctx.nodeId, ctx.newParentId, ctx.newParentPath, tempName, "", true);
                //Record the fact that we have to rename this folder and update the database later
                recordFolderMove(ctx);
                //Process any orphans that were waiting for this new folder
                processOrphans(ctx.nodeId);
            } else if (ctx.hasMoved) {
                //If a folder has been moved then we'll do nothing for now other than to make a note of 
                //it to process later
                if (isDebugEnabled) {
                    log.debug("This folder has moved. Recording for processing later.");
                }
                recordFolderMove(ctx);
            } else {
                if (isDebugEnabled) {
                    log.debug("This folder has been neither moved nor renamed. Skipping.");
                }
            }
        } else // isFile
        {
            if (isDebugEnabled) {
                log.debug("This node represents a file");
            }
            String newContentUrl = ctx.newContentUrl;
            boolean isContentModified = (ctx.isNew
                    || (newContentUrl != null && !newContentUrl.equals(ctx.currentContentUrl)));
            if (ctx.isNew && newContentUrl != null) {
                if (isDebugEnabled) {
                    log.debug("This file has never been received before");
                }
                //Copy the staged content file to the correct folder with a temporary name
                //Note that we can't simply *move* the staged file, as it's possible that more than one node
                //shares the same content URL (in the case of a copied node, for instance)
                String contentKey = TransferCommons.URLToPartName(newContentUrl);
                File stagedContent = fileTransferReceiver.getContents().get(contentKey);
                File newFile = new File(pathPrefix + ctx.newParentPath, tempName);
                if (isDebugEnabled) {
                    log.debug("Attempting to copy from staged file to new file (" + stagedContent.getPath() + " to "
                            + newFile.getPath() + ")");
                }
                try {
                    FileUtils.copyFile(stagedContent, newFile);
                } catch (IOException e) {
                    log.error("Failed to copy content", e);
                    throw new TransferFatalException(MSG_ERROR_COPYING_FILE,
                            new Object[] { stagedContent.getPath(), newFile.getPath() }, e);
                }
                //Store in the database
                if (isDebugEnabled) {
                    log.debug("File copied successfully. Updating database.");
                }
                dbHelper.createNodeInDB(ctx.nodeId, ctx.newParentId, ctx.newParentPath, tempName, newContentUrl,
                        false);
                //Record the fact that we need to rename this file later
                tempFilesToRename.put(ctx.nodeId, ctx);
            } else if (!ctx.isNew) {
                if (isContentModified) {
                    if (isDebugEnabled) {
                        log.debug("This file has been changed.");
                    }
                    //Copy the staged content file to the correct folder with a temporary name
                    String contentKey = TransferCommons.URLToPartName(newContentUrl);
                    File stagedContent = fileTransferReceiver.getContents().get(contentKey);
                    File newFile = new File(pathPrefix + ctx.newParentPath, tempName);
                    if (isDebugEnabled) {
                        log.debug("Attempting to copy from current file to new file (" + stagedContent.getPath()
                                + " to " + newFile.getPath() + ")");
                    }
                    try {
                        FileUtils.copyFile(stagedContent, newFile);
                    } catch (IOException e) {
                        log.error("Failed to copy content", e);
                        throw new TransferFatalException(MSG_ERROR_COPYING_FILE,
                                new Object[] { stagedContent.getPath(), newFile.getPath() }, e);
                    }
                    //Record the fact that we need to rename this file, delete the original file, and update the database later
                    if (isDebugEnabled) {
                        log.debug("File copied successfully. Recording the need to switch files later.");
                    }
                    existingFilesToReplace.put(ctx.nodeId, ctx);
                } else if (ctx.hasMoved) {
                    //Copy the current content to the correct folder with a temporary name
                    if (isDebugEnabled) {
                        log.debug("This file has been moved.");
                    }
                    File currentFile = new File(pathPrefix + ctx.currentParentPath, ctx.currentName);
                    File newFile = new File(pathPrefix + ctx.newParentPath, tempName);
                    if (isDebugEnabled) {
                        log.debug("Attempting to copy from current file to new file (" + currentFile.getPath()
                                + " to " + newFile.getPath() + ")");
                    }
                    try {
                        FileUtils.copyFile(currentFile, newFile);
                    } catch (IOException e) {
                        log.error("Failed to copy content", e);
                        throw new TransferFatalException(MSG_ERROR_COPYING_FILE,
                                new Object[] { currentFile.getPath(), newFile.getPath() }, e);
                    }
                    //Record the fact that we need to rename this file, delete the original file, and update the database later
                    if (isDebugEnabled) {
                        log.debug("File copied successfully. Recording the need to switch files later.");
                    }
                    existingFilesToReplace.put(ctx.nodeId, ctx);
                } else {
                    //neither moved nor updated - we don't need to do anything with this file.
                    if (isDebugEnabled) {
                        log.debug("This file has been neither moved nor changed. Skipping.");
                    }
                }
            }
        }
    }

    @Override
    protected void localHandleException(TransferManifestNode node, Throwable ex) {
        String pathPrefix = fileTransferReceiver.getDefaultReceivingroot();

        if (isDebugEnabled) {
            log.debug("Exception has occurred. Attempt to clean up has started.");
        }

        //Remove any temporary files that haven't been recorded in the database yet
        for (NodeContext fileToReplace : existingFilesToReplace.values()) {
            FileTransferInfoEntity parentEntity = dbHelper.findFileTransferInfoByNodeRef(fileToReplace.newParentId);
            if (parentEntity != null) {
                String tempFilePath = pathPrefix + parentEntity.getPath() + parentEntity.getContentName() + "/"
                        + fileToReplace.tempName;
                if (isDebugEnabled) {
                    log.debug("Attempting to delete temporary file: " + tempFilePath);
                }
                File tempFile = new File(tempFilePath);
                if (tempFile.exists()) {
                    if (tempFile.delete()) {
                        if (isDebugEnabled) {
                            log.debug("Successfully deleted temporary file: " + tempFilePath);
                        }
                    } else {
                        log.warn("Failed to delete temporary file " + tempFilePath
                                + "\nIt is highly recommended that action be taken to delete this file.");
                    }
                }
            }
        }

        //If there are any folders that were part way through the process of being deleted then
        //have a last-ditch attempt to delete them now
        try {
            removeDeletedFolders(pathPrefix);
        } catch (Throwable t) {
            //Nothing really to do. 
        }

        //Finally we'll have a last-ditch attempt at renaming temporary files to their proper name.
        try {
            renameTempFiles(pathPrefix);
        } catch (Throwable t) {
            //Nothing really to do. 
        }
    }

    private void renameTempFiles(String pathPrefix) {
        Collection<NodeContext> filesToRename = new ArrayList<NodeContext>(tempFilesToRename.values());
        for (NodeContext file : filesToRename) {
            FileTransferInfoEntity fileEntity = dbHelper.findFileTransferInfoByNodeRef(file.nodeId);
            if (fileEntity != null) {
                File sourceFile = new File(pathPrefix + fileEntity.getPath() + file.tempName);
                File targetFile = new File(pathPrefix + fileEntity.getPath() + file.newName);
                if (isDebugEnabled) {
                    log.debug("Attempting to rename temp file \"" + sourceFile.getPath() + "\" to \""
                            + targetFile.getPath() + "\"");
                }
                if (sourceFile.renameTo(targetFile)) {
                    if (isDebugEnabled) {
                        log.debug("Rename succeeded. Updating database.");
                    }
                    fileEntity.setContentName(file.newName);
                    dbHelper.updateFileTransferInfoByNodeRef(fileEntity);
                    tempFilesToRename.remove(file.nodeId);
                    //Log the effect that this has had...
                    if (file.isNew) {
                        logCreated(file.nodeId, file.newParentId,
                                pathPrefix + fileEntity.getPath() + fileEntity.getContentName(), false);
                    } else {
                        logMoved(file.nodeId, pathPrefix + file.currentParentPath + file.currentName,
                                file.newParentId, pathPrefix + fileEntity.getPath() + fileEntity.getContentName());
                    }
                } else {
                    if (isDebugEnabled) {
                        log.error("Rename FAILED");
                    }
                }
            }
        }
    }

    private void recordForSyncMode(NodeContext ctx) {
        if (isSync) {
            if (ctx.isFolder) {
                receivedFolderIds.add(ctx.nodeId);
            } else {
                receivedFileIds.add(ctx.nodeId);
            }
            String parentId = ctx.newParentId;
            Set<String> children = parentChildMap.get(parentId);
            if (children == null) {
                children = new TreeSet<String>();
                parentChildMap.put(parentId, children);
            }
            children.add(ctx.nodeId);
        }
    }

    private void recordFolderMove(NodeContext ctx) {
        foldersToMove.put(ctx.nodeId, ctx);
    }

    private void processOrphans(String nodeId) {
        if (isDebugEnabled) {
            log.debug("Processing orphans for folder " + nodeId);
        }
        List<NodeContext> relevantOrphans = orphans.get(nodeId);
        if (relevantOrphans != null) {
            FileTransferInfoEntity parentEntity = dbHelper.findFileTransferInfoByNodeRef(nodeId);
            String parentPath = parentEntity.getPath() + parentEntity.getContentName() + "/";
            for (NodeContext orphan : relevantOrphans) {
                orphan.newParentPath = parentPath;
                processContext(orphan);
            }
            orphans.remove(nodeId);
        }
    }

    private void recordOrphan(NodeContext ctx) {
        List<NodeContext> existingOrphans = orphans.get(ctx.newParentId);
        if (existingOrphans == null) {
            existingOrphans = new ArrayList<NodeContext>();
            orphans.put(ctx.newParentId, existingOrphans);
        }
        existingOrphans.add(ctx);
    }

    protected void logCreated(String sourceNode, String newParentNode, String parentPath, boolean orphan) {
        NodeRef srcNodeRef = new NodeRef(sourceNode);
        fileTransferReceiver.getProgressMonitor().logCreated(getTransferId(), srcNodeRef, srcNodeRef,
                new NodeRef(newParentNode), parentPath, orphan);
    }

    protected void logDeleted(String sourceNode, String parentPath) {
        NodeRef srcNodeRef = new NodeRef(sourceNode);
        fileTransferReceiver.getProgressMonitor().logDeleted(getTransferId(), srcNodeRef, srcNodeRef, parentPath);
    }

    protected void logUpdated(String sourceNode, String newPath) {
        NodeRef srcNodeRef = new NodeRef(sourceNode);
        fileTransferReceiver.getProgressMonitor().logUpdated(getTransferId(), srcNodeRef, srcNodeRef, newPath);
    }

    protected void logMoved(String sourceNode, String oldPath, String newParent, String newPath) {
        NodeRef srcNodeRef = new NodeRef(sourceNode);
        fileTransferReceiver.getProgressMonitor().logMoved(getTransferId(), srcNodeRef, srcNodeRef, oldPath,
                new NodeRef(newParent), newPath);
    }

    /**
     * A simple data holder that pulls together useful information about a node in one place.
     * @author Brian Remmington
     */
    private static class NodeContext {
        private static int renamingCounter = 1;

        public String nodeId;
        public boolean isFolder;
        public boolean isNew;
        public boolean isRenamed;
        public boolean hasMoved;
        public boolean parentHasChanged;
        public boolean parentAlreadyExists;

        public String currentName;
        @SuppressWarnings("unused")
        public String currentParentId;
        public String currentParentPath;
        public String currentContentUrl;

        public String newName;
        public String newParentId;
        public String newParentPath;
        public String newContentUrl;

        public String tempName;

        public static String getNextTempName() {
            return ".ftr" + (renamingCounter++);
        }

        public static NodeContext buildNodeContext(TransferManifestNormalNode node,
                FileTransferInfoEntity nodeEntity, FileTransferInfoEntity parentEntity) {
            NodeContext result = new NodeContext();
            //Pull some useful information out of the supplied node object
            result.newName = (String) node.getProperties().get(ContentModel.PROP_NAME);
            result.nodeId = node.getNodeRef().toString();
            result.isFolder = ContentModel.TYPE_FOLDER.equals(node.getAncestorType());
            result.newParentId = node.getPrimaryParentAssoc().getParentRef().toString();

            //Look up the node id in our database and extract some info from what we find
            result.isNew = (nodeEntity == null);
            result.isRenamed = (!result.isNew && !result.newName.equals(nodeEntity.getContentName()));
            result.parentHasChanged = (!result.isNew && !result.newParentId.equals(nodeEntity.getParent()));
            result.hasMoved = result.parentHasChanged || result.isRenamed;
            result.currentParentPath = result.isNew ? null : nodeEntity.getPath();
            result.currentParentId = result.isNew ? null : nodeEntity.getParent();
            result.currentContentUrl = result.isNew ? null : nodeEntity.getContentUrl();
            result.currentName = result.isNew ? null : nodeEntity.getContentName();

            //Look up the target parent node id in our database and extract some info from what we find
            result.parentAlreadyExists = (parentEntity != null);
            result.newParentPath = result.parentAlreadyExists
                    ? (parentEntity.getPath() + parentEntity.getContentName() + "/")
                    : null;
            result.tempName = getNextTempName();

            ContentData contentData = (ContentData) node.getProperties().get(ContentModel.PROP_CONTENT);
            result.newContentUrl = "";
            if (contentData != null) {
                result.newContentUrl = contentData.getContentUrl();
            }

            return result;
        }

        public static NodeContext buildNodeContext(TransferManifestDeletedNode node,
                FileTransferInfoEntity nodeEntity) {
            NodeContext result = new NodeContext();
            //Pull some useful information out of the supplied node object
            result.nodeId = node.getNodeRef().toString();

            //Look up the node id in our database and extract some info from what we find
            result.isNew = (nodeEntity == null);
            result.currentParentPath = result.isNew ? null : nodeEntity.getPath();
            result.currentParentId = result.isNew ? null : nodeEntity.getParent();
            result.currentContentUrl = result.isNew ? null : nodeEntity.getContentUrl();
            result.currentName = result.isNew ? null : nodeEntity.getContentName();

            //Look up the target parent node id in our database and extract some info from what we find
            result.tempName = getNextTempName();

            return result;
        }
    }
}