/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.transfer;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.model.Repository;
import org.alfresco.repo.security.authentication.AuthenticationComponent;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.repo.transfer.manifest.TransferManifestNodeFactory;
import org.alfresco.service.cmr.action.ActionService;
import org.alfresco.service.cmr.lock.LockService;
import org.alfresco.service.cmr.repository.AssociationRef;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.CopyService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.Path;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.search.CategoryService;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.security.MutableAuthenticationService;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.cmr.tagging.TaggingService;
import org.alfresco.service.cmr.transfer.TransferCallback;
import org.alfresco.service.cmr.transfer.TransferDefinition;
import org.alfresco.service.cmr.transfer.TransferEvent;
import org.alfresco.service.cmr.transfer.TransferEventBegin;
import org.alfresco.service.cmr.transfer.TransferException;
import org.alfresco.service.cmr.transfer.TransferReceiver;
import org.alfresco.service.cmr.transfer.TransferService;
import org.alfresco.service.cmr.transfer.TransferTarget;
import org.alfresco.service.descriptor.Descriptor;
import org.alfresco.service.descriptor.DescriptorService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.test_category.BaseSpringTestsCategory;
import org.alfresco.util.BaseAlfrescoSpringTest;
import org.alfresco.util.GUID;
import org.alfresco.util.Pair;
import org.alfresco.util.TempFileProvider;
import org.alfresco.util.testing.category.RedundantTests;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.springframework.test.context.transaction.TestTransaction;
import org.springframework.transaction.annotation.Transactional;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * Unit test for TransferServiceImpl
 * 
 * Contains some integration tests for the transfer service
 *
 * @author Mark Rogers
 */
@SuppressWarnings("deprecation")
@Category({BaseSpringTestsCategory.class})
public class TransferServiceImplTest extends BaseAlfrescoSpringTest
{
    private TransferService transferService;
    private ContentService contentService;
    private TransferServiceImpl2 transferServiceImpl;
    private SearchService searchService;
    private TransactionService transactionService;
    private TransferReceiver receiver;
    private TransferManifestNodeFactory transferManifestNodeFactory; 
    private PermissionService permissionService;
    private LockService lockService;
    private PersonService personService;
    private DescriptorService descriptorService;
    private CopyService copyService;
    private Descriptor serverDescriptor;
    private TaggingService taggingService; 
    private CategoryService categoryService;
    private Repository repositoryHelper;
    
    String COMPANY_HOME_XPATH_QUERY = "/{http://www.alfresco.org/model/application/1.0}company_home";
    String GUEST_HOME_XPATH_QUERY = "/{http://www.alfresco.org/model/application/1.0}company_home/{http://www.alfresco.org/model/application/1.0}guest_home";

    String REPO_ID_A = "RepoIdA";
    String REPO_ID_B;
    String REPO_ID_C = "RepoIdC";
    
    /**
     * Called during the transaction setup
     */
    @Before
    public void before() throws Exception
    {
        super.before();
        
        // Get the required services
        this.transferService = (TransferService)this.applicationContext.getBean("TransferService");
        this.contentService = (ContentService)this.applicationContext.getBean("ContentService");
        this.transferServiceImpl = (TransferServiceImpl2)this.applicationContext.getBean("transferService2");
        this.searchService = (SearchService)this.applicationContext.getBean("SearchService");
        this.transactionService = (TransactionService)this.applicationContext.getBean("TransactionService");
        this.nodeService = (NodeService) this.applicationContext.getBean("nodeService");
        this.contentService = (ContentService) this.applicationContext.getBean("contentService");
        this.authenticationService = (MutableAuthenticationService) this.applicationContext.getBean("authenticationService");
        this.actionService = (ActionService)this.applicationContext.getBean("actionService");
        this.permissionService = (PermissionService)this.applicationContext.getBean("permissionService");
        this.receiver = (TransferReceiver)this.applicationContext.getBean("transferReceiver");
        this.transferManifestNodeFactory = (TransferManifestNodeFactory)this.applicationContext.getBean("transferManifestNodeFactory");
        this.authenticationComponent = (AuthenticationComponent) this.applicationContext.getBean("authenticationComponent");
        this.lockService = (LockService) this.applicationContext.getBean("lockService");
        this.personService = (PersonService)this.applicationContext.getBean("PersonService");
        this.descriptorService = (DescriptorService)this.applicationContext.getBean("DescriptorService");
        this.copyService = (CopyService)this.applicationContext.getBean("CopyService");
        this.taggingService = ((TaggingService)this.applicationContext.getBean("TaggingService"));
        this.categoryService = (CategoryService)this.applicationContext.getBean("CategoryService");
        this.repositoryHelper = (Repository) this.applicationContext.getBean("repositoryHelper");
        
        this.serverDescriptor = descriptorService.getServerDescriptor();
        
        REPO_ID_B = descriptorService.getCurrentRepositoryDescriptor().getId();
        
        authenticationComponent.setSystemUserAsCurrentUser();
        assertNotNull("receiver is null", this.receiver);

        TestTransaction.flagForCommit();
        TestTransaction.end();
    }
    
    @Test
    public void testSetup()
    {
        assertEquals(
                "Must run without transactions",
                TxnReadState.TXN_NONE, AlfrescoTransactionSupport.getTransactionReadState());
    }

    /**
     * Test create target.
     * 
     * @throws Exception
     */
    @Test
    public void testCreateTarget() throws Exception
    {
        String name = "Test Transfer Target " + GUID.generate();
        String title = "title";
        String description = "description";
        String endpointProtocol = "http";
        String endpointHost = "localhost";
        int endpointPort = 8080;
        String endpointPath = "rhubarb";
        String username = "admin";
        char[] password = "password".toCharArray();


        /**
         * Now go ahead and create our first transfer target
         */
        TransferTarget ret = transferService.createAndSaveTransferTarget(name, title, description, endpointProtocol, endpointHost, endpointPort, endpointPath, username, password);
        assertNotNull("return value is null", ret);
        assertNotNull("node ref is null", ret.getNodeRef());

        //titled aspect
        assertEquals("name not equal", ret.getName(), name);
        assertEquals("title not equal", ret.getTitle(), title);
        assertEquals("description not equal", ret.getDescription(), description);

        // endpoint 
        assertEquals("endpointProtocol not equal", ret.getEndpointProtocol(), endpointProtocol);
        assertEquals("endpointHost not equal", ret.getEndpointHost(), endpointHost);
        assertEquals("endpointPort not equal", ret.getEndpointPort(), endpointPort);
        assertEquals("endpointPath not equal", ret.getEndpointPath(), endpointPath);

        // authentication
        assertEquals("username not equal", ret.getUsername(), username);
        char[] password2 = ret.getPassword();

        assertEquals(password.length, password2.length);

        for(int i = 0; i < password.length; i++)
        {
            if(password[i] != password2[i])
            {
                fail("password not equal:" + new String(password) + new String(password2));
            }
        }

        /**
         * Negative test - try to create a transfer target with a name that's already used.
         */
        try 
        {    
            transferService.createAndSaveTransferTarget(name, title, description, endpointProtocol, endpointHost, endpointPort, endpointPath, username, password);
            fail("duplicate name not detected");
        }
        catch (TransferException e)
        {
            // expect to go here
        }
    }
    
    /**
     * Test create target via in memory data object.
     * 
     * @throws Exception
     */
    @Test
    public void testCreateTargetSyntax2() throws Exception
    {
        String name = "Test Transfer Target " + GUID.generate();
        String title = "title";
        String description = "description";
        String endpointProtocol = "http";
        String endpointHost = "localhost";
        int endpointPort = 8080;
        String endpointPath = "rhubarb";
        String username = "admin";
        char[] password = "password".toCharArray();

        /**
         * Now go ahead and create our first transfer target
         */
        TransferTarget newValue = transferService.createTransferTarget(name);
        newValue.setDescription(description);
        newValue.setEndpointHost(endpointHost);
        newValue.setEndpointPort(endpointPort);
        newValue.setEndpointPath(endpointPath);
        newValue.setEndpointProtocol(endpointProtocol);
        newValue.setPassword(password);
        newValue.setTitle(title);
        newValue.setUsername(username);

        TransferTarget ret = transferService.saveTransferTarget(newValue);

        assertNotNull("return value is null", ret);
        assertNotNull("node ref is null", ret.getNodeRef());

        //titled aspect
        assertEquals("name not equal", ret.getName(), name);
        assertEquals("title not equal", ret.getTitle(), title);
        assertEquals("description not equal", ret.getDescription(), description);

        // endpoint 
        assertEquals("endpointProtocol not equal", ret.getEndpointProtocol(), endpointProtocol);
        assertEquals("endpointHost not equal", ret.getEndpointHost(), endpointHost);
        assertEquals("endpointPort not equal", ret.getEndpointPort(), endpointPort);
        assertEquals("endpointPath not equal", ret.getEndpointPath(), endpointPath);

        // authentication
        assertEquals("username not equal", ret.getUsername(), username);
        char[] password2 = ret.getPassword();

        assertEquals(password.length, password2.length);

        for(int i = 0; i < password.length; i++)
        {
            if(password[i] != password2[i])
            {
                fail("password not equal:" + new String(password) + new String(password2));
            }
        }

        /**
         * Negative test - try to create a transfer target with a name that's already used.
         */
        try 
        {

            transferService.createAndSaveTransferTarget(name, title, description, endpointProtocol, endpointHost, endpointPort, endpointPath, username, password);
            fail("duplicate name not detected");
        }
        catch (TransferException e)
        {
            // expect to go here
        }
    }
    
    /**
     * Test of Get TransferTargets
     * 
     * @throws Exception
     */
    @Test
    public void testGetTransferTargets() throws Exception
    {
        String nameA = "Test Transfer Target " + GUID.generate();
        String nameB = "Test Transfer Target " + GUID.generate();
        String title = "title";
        String description = "description";
        String endpointProtocol = "http";
        String endpointHost = "localhost";
        int endpointPort = 8080;
        String endpointPath = "rhubarb";
        String username = "admin";
        char[] password = "password".toCharArray();

        /**
         * Now go ahead and create our first transfer target
         */
        TransferTarget targetA = transferService.createAndSaveTransferTarget(nameA, title, description, endpointProtocol, endpointHost, endpointPort, endpointPath, username, password);
        TransferTarget targetB = transferService.createAndSaveTransferTarget(nameB, title, description, endpointProtocol, endpointHost, endpointPort, endpointPath, username, password);

        Set<TransferTarget> targets = transferService.getTransferTargets();
        assertTrue("targets is empty", targets.size() > 0);
        assertTrue("didn't find target A", targets.contains(targetA) );
        assertTrue("didn't find target B", targets.contains(targetB));
        for(TransferTarget target : targets)
        {
            System.out.println("found target: " + target.getName());
        }
    }
    
    /**
     * Test that if someone copies a transfer group using a client app then the getTransferTarget operations still succeed
     * 
     * @throws Exception
     */
    @Test
    public void testALF6565() throws Exception
    {
        String nameA = GUID.generate();
        String nameB = GUID.generate();
        String title = "title";
        String description = "description";
        String endpointProtocol = "http";
        String endpointHost = "localhost";
        int endpointPort = 8080;
        String endpointPath = "rhubarb";
        String username = "admin";
        char[] password = "password".toCharArray();

        /**
         * Now go ahead and create our first transfer target
         */
        TransferTarget targetA = transferService.createAndSaveTransferTarget(nameA, title, description, endpointProtocol, endpointHost, endpointPort, endpointPath, username, password);
        TransferTarget targetB = transferService.createAndSaveTransferTarget(nameB, title, description, endpointProtocol, endpointHost, endpointPort, endpointPath, username, password);

        NodeRef transferHome = transferServiceImpl.getTransferHome();
        NodeRef defaultGroup = transferServiceImpl.getDefaultGroup();
        assertNotNull(defaultGroup);
        copyService.copyAndRename(defaultGroup, transferHome, ContentModel.ASSOC_CONTAINS, QName.createQName("test"), true);
        
        Set<TransferTarget> targets = transferService.getTransferTargets();

        int targetACount = 0;
        int targetBCount = 0;
        for (TransferTarget target : targets)
        {
            if (target.getName().equals(nameA)) ++targetACount;
            if (target.getName().equals(nameB)) ++targetBCount;
        }
        assertEquals(2, targetACount);
        assertEquals(2, targetBCount);

        assertEquals(targetA.getNodeRef(), transferService.getTransferTarget(nameA).getNodeRef());
        assertEquals(targetB.getNodeRef(), transferService.getTransferTarget(nameB).getNodeRef());
    }
    
    /**
     * Test of Get All Transfer Targets By Group
     */
    //TODO Test not complete - can't yet put targets in different groups
    @Test
    public void testGetAllTransferTargetsByGroup() throws Exception
    {
        String getMe = "Test Transfer Target " + GUID.generate();
        String title = "title";
        String description = "description";
        String endpointProtocol = "http";
        String endpointHost = "localhost";
        int endpointPort = 8080;
        String endpointPath = "rhubarb";
        String username = "admin";
        char[] password = "password".toCharArray();

        /**
         * Now go ahead and create our first transfer target
         */
        transferService.createAndSaveTransferTarget(getMe, title, description, endpointProtocol, endpointHost, endpointPort, endpointPath, username, password);

        //TODO MER Test assumes an English Locale.   (Fails with French)
        Set<TransferTarget> targets = transferService.getTransferTargets("Default Group");
        assertTrue("targets is empty", targets.size() > 0);     
        /**
         * Negative test - group does not exist
         */
        try {
            targets = transferService.getTransferTargets("Rubbish");
            assertTrue("targets is empty", targets.size() > 0);
            fail("group does not exist");
        }
        catch (TransferException te)
        {
            // expect to go here
        }

    }
    
    /**
     * 
     */
    @Test
    public void testUpdateTransferTarget() throws Exception
    {
        String updateMe = "Test Transfer Target " + GUID.generate();
        String title = "title";
        String description = "description";
        String endpointProtocol = "http";
        String endpointHost = "localhost";
        int endpointPort = 8080;
        String endpointPath = "rhubarb";
        String username = "admin";
        char[] password = "password".toCharArray();

        /**
         * Create our transfer target
         */
        TransferTarget target = transferService.createAndSaveTransferTarget(updateMe, title, description, endpointProtocol, endpointHost, endpointPort, endpointPath, username, password);

        /*
         * Now update with exactly the same values.
         */
        TransferTarget update1 = transferService.saveTransferTarget(target);

        assertNotNull("return value is null", update1);
        assertNotNull("node ref is null", update1.getNodeRef());

        //titled aspect
        assertEquals("name not equal", update1.getName(), updateMe);
        assertEquals("title not equal", update1.getTitle(), title);
        assertEquals("description not equal", update1.getDescription(), description);

        // endpoint 
        assertEquals("endpointProtocol not equal", update1.getEndpointProtocol(), endpointProtocol);
        assertEquals("endpointHost not equal", update1.getEndpointHost(), endpointHost);
        assertEquals("endpointPort not equal", update1.getEndpointPort(), endpointPort);
        assertEquals("endpointPath not equal", update1.getEndpointPath(), endpointPath);

        // authentication
        assertEquals("username not equal", update1.getUsername(), username);
        char[] pass = update1.getPassword();

        assertEquals(password.length, pass.length);

        for(int i = 0; i < password.length; i++)
        {
            if(password[i] != pass[i])
            {
                fail("password not equal:" + new String(password) + new String(pass));
            }
        }

        /**
         * Now update with different values
         */
        String title2 = "Two";
        String description2 = "descriptionTwo";
        String endpointProtocol2 = "https";
        String endpointHost2 = "1.0.0.127";
        int endpointPort2 = 4040;
        String endpointPath2 = "custard";
        String username2 = "admin_two";
        char[] password2 = "two".toCharArray();

        target.setDescription(description2);
        target.setTitle(title2);
        target.setEndpointHost(endpointHost2);
        target.setEndpointPath(endpointPath2);
        target.setEndpointPort(endpointPort2);
        target.setEndpointProtocol(endpointProtocol2);
        target.setPassword(password2);
        target.setUsername(username2);

        TransferTarget update2 = transferService.saveTransferTarget(target);
        assertNotNull("return value is null", update2);
        assertNotNull("node ref is null", update2.getNodeRef());

        //titled aspect
        assertEquals("name not equal", update2.getName(), updateMe);
        assertEquals("title not equal", update2.getTitle(), title2);
        assertEquals("description not equal", update2.getDescription(), description2);

        // endpoint 
        assertEquals("endpointProtocol not equal", update2.getEndpointProtocol(), endpointProtocol2);
        assertEquals("endpointHost not equal", update2.getEndpointHost(), endpointHost2);
        assertEquals("endpointPort not equal", update2.getEndpointPort(), endpointPort2);
        assertEquals("endpointPath not equal", update2.getEndpointPath(), endpointPath2);

        // authentication
        assertEquals("username not equal", update2.getUsername(), username2);
        pass = update2.getPassword();

        assertEquals(password2.length, pass.length);

        for(int i = 0; i < pass.length; i++)
        {
            if(pass[i] != password2[i])
            {
                fail("password not equal:" + new String(pass) + new String(password2));
            }
        }
    }
    
    /**
     * 
     */
    @Test
    public void testDeleteTransferTarget() throws Exception
    {
        String deleteMe = "deleteMe";
        String title = "title";
        String description = "description";
        String endpointProtocol = "http";
        String endpointHost = "localhost";
        int endpointPort = 8080;
        String endpointPath = "rhubarb";
        String username = "admin";
        char[] password = "password".toCharArray();

        /**
         * Now go ahead and create our first transfer target
         */
        transferService.createAndSaveTransferTarget(deleteMe, title, description, endpointProtocol, endpointHost, endpointPort, endpointPath, username, password);

        transferService.deleteTransferTarget(deleteMe);

        /**
         * Negative test - try to delete the transfer target we deleted above.
         */
        try 
        {
            transferService.deleteTransferTarget(deleteMe);
            fail("duplicate name not detected");
        }
        catch (TransferException e)
        {
            // expect to go here
        }   

        /**
         * Negative test - try to delete a transfer target that has never existed
         */
        try 
        {
            transferService.deleteTransferTarget("rubbish");

            fail("rubbish deleted");
        }
        catch (TransferException e)
        {
            // expect to go here
        }

    }
    
    @Test
    public void testEnableTransferTarget() throws Exception
    {
        String targetName = "enableMe";

        /**
         * Now go ahead and create our first transfer target
         */
        TransferTarget enableMe = createTransferTarget(targetName);
        try 
        {
            /**
             * Check a new target is enabled
             */
            TransferTarget target = transferService.getTransferTarget(targetName);
            assertTrue("new target is not enabled", enableMe.isEnabled());

            /**
             * Diasble the target
             */
            transferService.enableTransferTarget(targetName, false);
            target = transferService.getTransferTarget(targetName);
            assertFalse("target is not disabled", target.isEnabled());

            /**
             * Now re-enable the target
             */
            transferService.enableTransferTarget(targetName, true);
            target = transferService.getTransferTarget(targetName);
            assertTrue("re-enabled target is not enabled", target.isEnabled());
        }
        finally
        {
            transferService.deleteTransferTarget(targetName);
        }
    }

    /**
     * Test the transfer method by sending one node (CRUD).
     * 
     * Step 1: Create a new node (No content)
     * transfer
     * 
     * Step 2: Update Node title property
     * transfer
     *  
     * Step 3: Update Content property (add content)
     * transfer
     * 
     * Step 4: Transfer again
     * transfer (Should transfer but not request the content item)
     * 
     * Step 5: Update Content property (update content)
     *      
     * Step 6: Delete the node
     * 
     * Step 7: Negative test : transfer no nodes
     * transfer (should throw exception)
     * 
     * Step 8: Negative test : transfer to a disabled transfer target
     * transfer (should throw exception)
     * 
     * This is a unit test so it does some shenanigans to send to the same instance of alfresco.
     */
    @Test
    public void testTransferOneNode() throws Exception
    {
        final String CONTENT_TITLE = "ContentTitle";
        final String CONTENT_TITLE_UPDATED = "ContentTitleUpdated";
        final Locale CONTENT_LOCALE = Locale.GERMAN; 
        final String CONTENT_STRING = "Hello World";
        final String CONTENT_UPDATE_STRING = "Foo Bar";
        final String targetName = "testXferOneNode";

        final RetryingTransactionHelper tran = transactionService.getRetryingTransactionHelper();

        class TestContext
        {
            TransferTarget transferMe;
            NodeRef contentNodeRef;
            NodeRef destNodeRef;
        };

        /**
         * Unit test kludge to transfer from guest home to company home
         */
        final UnitTestTransferManifestNodeFactory testNodeFactory = unitTestKludgeToTransferGuestHomeToCompanyHome();
        
        DescriptorService mockedDescriptorService = getMockDescriptorService(REPO_ID_A);
        transferServiceImpl.setDescriptorService(mockedDescriptorService);

        RetryingTransactionCallback<TestContext> setupCB = new RetryingTransactionCallback<TestContext>()
        {
            @Override
            public TestContext execute() throws Throwable
            {
                TestContext ctx = new TestContext();
                
                NodeRef guestHome = repositoryHelper.getGuestHome();

                /**
                 * Create a test node that we will read and write
                 */
                String name = GUID.generate();
                ChildAssociationRef child = nodeService.createNode(guestHome, ContentModel.ASSOC_CONTAINS, QName.createQName(name), ContentModel.TYPE_CONTENT);
                ctx.contentNodeRef = child.getChildRef();
                nodeService.setProperty(ctx.contentNodeRef, ContentModel.PROP_TITLE, CONTENT_TITLE);   
                nodeService.setProperty(ctx.contentNodeRef, ContentModel.PROP_NAME, name);

                if(!transferService.targetExists(targetName))
                {
                    ctx.transferMe = createTransferTarget(targetName);
                }
                else
                {
                    ctx.transferMe = transferService.getTransferTarget(targetName);
                }
                transferService.enableTransferTarget(targetName, true);

                return ctx;
            } 
        };
        final TestContext testContext = tran.doInTransaction(setupCB); 

        /**
         * Step 1: Transfer our node which has no content
         */ 
        logger.debug("First transfer - create new node (no content yet)");
        RetryingTransactionCallback<Void> transferCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                TransferDefinition definition = new TransferDefinition();
                Set<NodeRef>nodes = new HashSet<NodeRef>();
                nodes.add(testContext.contentNodeRef);
                definition.setNodes(nodes);
                transferService.transfer(targetName, definition);
                return null;
            }
        };
        tran.doInTransaction(transferCB);
        
        RetryingTransactionCallback<Void> validateStep1CB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                // Now validate that the target node exists and has similar properties to the source
                testContext.destNodeRef = testNodeFactory.getMappedNodeRef( testContext.contentNodeRef);
                assertFalse("unit test stuffed up - comparing with self",  testContext.destNodeRef.equals( testContext.transferMe.getNodeRef()));
                assertTrue("dest node ref does not exist", nodeService.exists( testContext.destNodeRef));
                assertEquals("title is wrong", (String)nodeService.getProperty( testContext.destNodeRef, ContentModel.PROP_TITLE), CONTENT_TITLE); 
                assertEquals("type is wrong", nodeService.getType( testContext.contentNodeRef), nodeService.getType( testContext.destNodeRef));
                
                // Check the modified time of the destination node is the same as the source node.
                Date destModifiedDate = (Date)nodeService.getProperty( testContext.destNodeRef, ContentModel.PROP_MODIFIED);
                Date srcModifiedDate = (Date)nodeService.getProperty( testContext.contentNodeRef, ContentModel.PROP_MODIFIED);
                
                logger.debug("srcModifiedDate : " + srcModifiedDate + " destModifiedDate : " + destModifiedDate);
                assertTrue("dest modified date is not correct", destModifiedDate.compareTo(srcModifiedDate)== 0);
                
                Date destCreatedDate = (Date)nodeService.getProperty( testContext.destNodeRef, ContentModel.PROP_CREATED);
                Date srcCreatedDate = (Date)nodeService.getProperty( testContext.contentNodeRef, ContentModel.PROP_CREATED);
                
                logger.debug("srcCreatedDate : " + srcCreatedDate + " destCreatedDate : " + destCreatedDate);
                assertTrue("dest created date is not correct", destCreatedDate.compareTo(srcCreatedDate)== 0);
                
                // Check injected transferred aspect.
                assertNotNull("transferredAspect", (String)nodeService.getProperty( testContext.destNodeRef, TransferModel.PROP_REPOSITORY_ID)); 
                
                // Now set up the next test which is to change the title 
                nodeService.setProperty( testContext.contentNodeRef, ContentModel.PROP_TITLE, CONTENT_TITLE_UPDATED);   
                return null;
            }
        };
        tran.doInTransaction(validateStep1CB);
        
        /**
         * Step 2:
         * Transfer our node again - so this is an update of the title property
         */
        logger.debug("Second transfer - update title property (no content yet)");
        tran.doInTransaction(transferCB);
     
        RetryingTransactionCallback<Void> validateStep2CB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {

                // Now validate that the target node exists and has similar properties to the source
                assertFalse("unit test stuffed up - comparing with self", testContext.destNodeRef.equals(testContext.transferMe.getNodeRef()));
                assertTrue("dest node ref does not exist", nodeService.exists(testContext.destNodeRef));
                assertEquals("title is wrong", (String)nodeService.getProperty(testContext.destNodeRef, ContentModel.PROP_TITLE), CONTENT_TITLE_UPDATED); 
                assertEquals("type is wrong", nodeService.getType(testContext.contentNodeRef), nodeService.getType(testContext.destNodeRef));
                
                // Check the modified time of the destination node is the same as the source node.
                Date destModifiedDate = (Date)nodeService.getProperty(testContext.destNodeRef, ContentModel.PROP_MODIFIED);
                Date srcModifiedDate = (Date)nodeService.getProperty(testContext.contentNodeRef, ContentModel.PROP_MODIFIED);

                logger.debug("srcModifiedDate : " + srcModifiedDate + " destModifiedDate : " + destModifiedDate);
                assertTrue("after update, modified date is not correct", destModifiedDate.compareTo(srcModifiedDate) == 0);
                
                Date destCreatedDate = (Date)nodeService.getProperty(testContext.destNodeRef, ContentModel.PROP_CREATED);
                Date srcCreatedDate = (Date)nodeService.getProperty(testContext.contentNodeRef, ContentModel.PROP_CREATED);
                
                logger.debug("srcCreatedDate : " + srcCreatedDate + " destCreatedDate : " + destCreatedDate);
                assertTrue("after update, created date is not correct", destCreatedDate.compareTo(srcCreatedDate)== 0);
          
                // Check injected transferred aspect.
                assertNotNull("transferredAspect", (String)nodeService.getProperty(testContext.destNodeRef, TransferModel.PROP_REPOSITORY_ID)); 
              
                return null;
            }
        };

        tran.doInTransaction(validateStep2CB);
        
        /**
         * Step 3 - update to add content
         */
        
        RetryingTransactionCallback<Void> step3WriteContentCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                ContentWriter writer = contentService.getWriter(testContext.contentNodeRef, ContentModel.PROP_CONTENT, true);
                writer.setLocale(CONTENT_LOCALE);
                writer.putContent(CONTENT_STRING);

                return null;
            }
        };

        tran.doInTransaction(step3WriteContentCB);

       
        logger.debug("Transfer again - this is an update to add new content");
        tran.doInTransaction(transferCB);
        
        RetryingTransactionCallback<Void> validateStep3CB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                ContentReader reader = contentService.getReader(testContext.destNodeRef, ContentModel.PROP_CONTENT);
                assertNotNull("reader is null", reader);
                String contentStr = reader.getContentString();
                assertEquals("Content is wrong", contentStr, CONTENT_STRING);

              
                return null;
            }
        };

        tran.doInTransaction(validateStep3CB);
  
        /**
         * Step 4:
         * Now transfer nothing - content items do not need to be transferred since its already on 
         * the destination.
         */
        logger.debug("Transfer again - with no new content");
        tran.doInTransaction(transferCB);
        
        RetryingTransactionCallback<Void> validateStep4CB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                // Now validate that the target node still exists and in particular that the old content is still there
                assertFalse("unit test stuffed up - comparing with self", testContext.destNodeRef.equals(testContext.transferMe.getNodeRef()));
                assertTrue("dest node ref does not exist", nodeService.exists(testContext.destNodeRef));
                assertEquals("title is wrong", (String)nodeService.getProperty(testContext.destNodeRef, ContentModel.PROP_TITLE), CONTENT_TITLE_UPDATED); 
                assertEquals("type is wrong", nodeService.getType(testContext.contentNodeRef), nodeService.getType(testContext.destNodeRef));
                
                ContentReader reader = contentService.getReader(testContext.destNodeRef, ContentModel.PROP_CONTENT);
                assertNotNull("reader is null", reader);
                String contentStr = reader.getContentString();
                assertEquals("Content is wrong", contentStr, CONTENT_STRING);
                
                // Check injected transferred aspect.
                assertNotNull("transferredAspect", (String)nodeService.getProperty(testContext.destNodeRef, TransferModel.PROP_REPOSITORY_ID));               
                return null;
            }
        };

        tran.doInTransaction(validateStep4CB);

                    
        /**
         * Step 5 - update content through transfer
         */
        
        RetryingTransactionCallback<Void> step5UpdateContentCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                ContentWriter writer = contentService.getWriter(testContext.contentNodeRef, ContentModel.PROP_CONTENT, true);
                writer.setLocale(CONTENT_LOCALE);
                writer.putContent(CONTENT_UPDATE_STRING);
              
                return null;
            }
        };

        tran.doInTransaction(step5UpdateContentCB);

        
        logger.debug("Transfer again - this is an update to add new content");
        tran.doInTransaction(transferCB);
        
        RetryingTransactionCallback<Void> validateStep5CB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                ContentReader reader = contentService.getReader(testContext.destNodeRef, ContentModel.PROP_CONTENT);
                assertNotNull("reader is null", reader);
                String contentStr = reader.getContentString();
                assertEquals("Content is wrong", CONTENT_UPDATE_STRING, contentStr);

              
                return null;
            }
        };

        tran.doInTransaction(validateStep5CB);
      
        /**
         * Step 6
         * Delete the node through transfer of the archive node
         */
        logger.debug("Transfer again - to delete a node through transferring an archive node");
        RetryingTransactionCallback<Void> step6CB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                nodeService.deleteNode(testContext.contentNodeRef);
                return null;
            }
        };

        tran.doInTransaction(step6CB);
        
        RetryingTransactionCallback<Void> transferDeletedCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                NodeRef deletedContentNodeRef = new NodeRef(StoreRef.STORE_REF_ARCHIVE_SPACESSTORE, testContext.contentNodeRef.getId());
                
                TransferDefinition definition = new TransferDefinition();
                Set<NodeRef> nodesToRemove = new HashSet<NodeRef>();
                nodesToRemove.add(deletedContentNodeRef);
                definition.setNodesToRemove(nodesToRemove);
                transferService.transfer(targetName, definition);
              
                return null;
            }
        };

        tran.doInTransaction(transferDeletedCB);
       
        RetryingTransactionCallback<Void> validateStep6CB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                assertFalse("dest node still exists", nodeService.exists(testContext.destNodeRef));              
                return null;
            }
        };

        tran.doInTransaction(validateStep6CB);

        /**
          * Step 7
          * Negative test transfer nothing
          */
        logger.debug("Transfer again - with no content - should throw exception");
        try
        {
                TransferDefinition definition = new TransferDefinition();
                Set<NodeRef>nodes = new HashSet<NodeRef>();
                definition.setNodes(nodes);
                transferService.transfer(targetName, definition);
                fail("exception not thrown");
        }
        catch(TransferException te)
        {
            // expect to go here
        }
       
        /**
         * Step 7: Negative test : transfer to a disabled transfer target
         * transfer (should throw exception)
         */ 
        logger.debug("Transfer again - with no content - should throw exception");
        try
        {
                transferService.enableTransferTarget(targetName, false);
                TransferDefinition definition = new TransferDefinition();
                
                Set<NodeRef>nodes = new HashSet<NodeRef>();
                nodes.add(testContext.contentNodeRef);
                definition.setNodes(nodes);
                transferService.transfer(targetName, definition);
                fail("target not enabled exception not thrown");
        }
        catch(TransferException te)
        {
           // expect to go here
           assertTrue("check contents of exception message :" + te.toString(), te.getCause().getMessage().contains("enabled"));
        }
     }
    
    /**
     * Test the transfer method w.r.t. moving a node.
     * 
     * Step 1.
     * Move by changing the parent's node ref.
     *  
     * This is a unit test so it does some shenanigans to send to the same instance of alfresco.
     */
    @Test
    public void testMoveNode() throws Exception
    {
        final String CONTENT_TITLE = "ContentTitle";
        final String CONTENT_TITLE_UPDATED = "ContentTitleUpdated";
        final Locale CONTENT_LOCALE = Locale.GERMAN; 
        final String CONTENT_STRING = "Hello";

        final RetryingTransactionHelper tran = transactionService.getRetryingTransactionHelper();

        /**
         * Now go ahead and create our first transfer target
         */
        final String targetName = "testTransferMoveNode";
        
        class TestContext
        {
            TransferTarget transferMe;
            NodeRef contentNodeRef;
            NodeRef parentNodeRef;
            NodeRef destNodeRef;
            NodeRef moveToNodeRef;
        };

        /**
         * Unit test kludge to transfer from guest home to company home
         */
        final UnitTestTransferManifestNodeFactory testNodeFactory = unitTestKludgeToTransferGuestHomeToCompanyHome();

        RetryingTransactionCallback<TestContext> setupCB = new RetryingTransactionCallback<TestContext>()
        {
            @Override
            public TestContext execute() throws Throwable
            {
                TestContext ctx = new TestContext();

                NodeRef guestHome = repositoryHelper.getGuestHome();

                /**
                 * Create a test node that we will read and write
                 */
                String name = GUID.generate();

                ChildAssociationRef newParent = nodeService.createNode(guestHome, ContentModel.ASSOC_CONTAINS, QName.createQName(name), ContentModel.TYPE_FOLDER);
                ctx.parentNodeRef = newParent.getChildRef();
                nodeService.setProperty(ctx.parentNodeRef, ContentModel.PROP_TITLE, CONTENT_TITLE);   
                nodeService.setProperty(ctx.parentNodeRef, ContentModel.PROP_NAME, name);

                ChildAssociationRef child = nodeService.createNode(ctx.parentNodeRef, ContentModel.ASSOC_CONTAINS, QName.createQName("TransferOneNode"), ContentModel.TYPE_CONTENT);
                ctx.contentNodeRef = child.getChildRef();
                nodeService.setProperty(ctx.contentNodeRef, ContentModel.PROP_TITLE, CONTENT_TITLE);   
                nodeService.setProperty(ctx.contentNodeRef, ContentModel.PROP_NAME, "TransferOneNode");

                ChildAssociationRef moveTo = nodeService.createNode(ctx.parentNodeRef, ContentModel.ASSOC_CONTAINS, QName.createQName("moveTo"), ContentModel.TYPE_FOLDER);
                ctx.moveToNodeRef = moveTo.getChildRef();
                nodeService.setProperty(ctx.moveToNodeRef, ContentModel.PROP_TITLE, CONTENT_TITLE);   
                nodeService.setProperty(ctx.moveToNodeRef, ContentModel.PROP_NAME, "moveTo");

                if(!transferService.targetExists(targetName))
                {
                    ctx.transferMe = createTransferTarget(targetName);
                }
                else
                {
                    ctx.transferMe = transferService.getTransferTarget(targetName);
                }
                transferService.enableTransferTarget(targetName, true);


                return ctx;
            } 
        };
        
        final TestContext testContext = tran.doInTransaction(setupCB); 

        DescriptorService mockedDescriptorService = getMockDescriptorService(REPO_ID_A);
        transferServiceImpl.setDescriptorService(mockedDescriptorService);

        RetryingTransactionCallback<Void> firstTransferCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                logger.debug("First transfer - create new node (no content yet)");

                /**
                 * Step 0: Transfer our node which has no content
                 */

                TransferDefinition definition = new TransferDefinition();
                Set<NodeRef>nodes = new HashSet<NodeRef>();
                nodes.add(testContext.parentNodeRef);
                nodes.add(testContext.contentNodeRef);
                nodes.add(testContext.moveToNodeRef);
                definition.setNodes(nodes);
                transferService.transfer(targetName, definition);

                return null;
            }
        };

        tran.doInTransaction(firstTransferCB);

        RetryingTransactionCallback<Void> validateTransferCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {

                // Now validate that the target node exists and has similar properties to the source
                NodeRef destNodeRef = testNodeFactory.getMappedNodeRef(testContext.contentNodeRef);
                NodeRef destParentNodeRef = testNodeFactory.getMappedNodeRef(testContext.parentNodeRef);

                ChildAssociationRef destParent = nodeService.getPrimaryParent(destNodeRef);
                assertEquals("parent node ref not correct prior to test", destParentNodeRef, destParent.getParentRef());

                return null;
            }
        };

        tran.doInTransaction(validateTransferCB);


        /**
         * Step 1: Move a node through transfer
         * Move the destination node
         * transfer (Should transfer the destination node back)
         */

        RetryingTransactionCallback<Void> moveNodeCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                logger.debug("Transfer again with moved node");

                // Move the node up one level on the destination.
                nodeService.moveNode(testContext.contentNodeRef, testContext.moveToNodeRef, ContentModel.ASSOC_CONTAINS,  QName.createQName("testOneNode"));   
                return null;
            }
        };

        tran.doInTransaction(moveNodeCB);

        RetryingTransactionCallback<Void> secondTransferCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                logger.debug("Second transfer");

                TransferDefinition definition = new TransferDefinition();
                Set<NodeRef>nodes = new HashSet<NodeRef>();
                nodes.add(testContext.contentNodeRef);
                definition.setNodes(nodes);
                transferService.transfer(targetName, definition);

                return null;
            }
        };

        tran.doInTransaction(secondTransferCB);

        RetryingTransactionCallback<Void> secondValidateCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                // Now validate that the target node exists and has similar properties to the source
                NodeRef destNodeRef = testNodeFactory.getMappedNodeRef(testContext.contentNodeRef);
                NodeRef destParentNodeRef = testNodeFactory.getMappedNodeRef(testContext.moveToNodeRef);

                ChildAssociationRef destParent = nodeService.getPrimaryParent(destNodeRef);
                assertEquals("node not moved", destParentNodeRef, destParent.getParentRef());

                return null;
            }
        };

        tran.doInTransaction(secondValidateCB);

    } // test move node


    /**
     * Test the transfer method by sending a graph of nodes.
     * 
     * This is a unit test so it does some shenanigans to send to he same instance of alfresco.
     */
    @Test
    public void testManyNodes() throws Exception
    {        
        final RetryingTransactionHelper tran = transactionService.getRetryingTransactionHelper();
        
        final String CONTENT_TITLE = "ContentTitle";
        final String CONTENT_TITLE_UPDATED = "ContentTitleUpdated";
        final String CONTENT_NAME = "Demo Node 1";
        final Locale CONTENT_LOCALE = Locale.GERMAN; 
        final String CONTENT_STRING = "The quick brown fox";
        final Set<NodeRef>nodes = new HashSet<NodeRef>();

        final String targetName = "testManyNodes";
        
        class TestContext
        {
            TransferTarget transferMe;
            NodeRef nodeA = null;
            NodeRef nodeB = null;
            NodeRef nodeAA = null;
            NodeRef nodeAB = null;
            NodeRef nodeABA = null;
            NodeRef nodeABB = null;
            NodeRef nodeABC = null;
        };
        
        /**
         * Unit test kludge to transfer from guest home to company home
         */
        final UnitTestTransferManifestNodeFactory testNodeFactory = unitTestKludgeToTransferGuestHomeToCompanyHome();
        
        TransferTarget transferMe;
        
        RetryingTransactionCallback<TestContext> setupCB = new RetryingTransactionCallback<TestContext>()
        {
            @Override
            public TestContext execute() throws Throwable
            {
                TestContext ctx = new TestContext();
                
               NodeRef guestHome = repositoryHelper.getGuestHome();
       
               /**
                * Create a test node that we will read and write
                */
               String guid = GUID.generate();
               
               /**
                * Create a tree
                * ManyNodesRoot
                * AC (Content Node)
                * A (Folder)
                * --AA
                * --AB (Folder)
                * ----ABA (Folder)
                * -------- 100+ nodes
                * ----ABB
                * ----ABC   
                * B
                */
               ChildAssociationRef child;
               
               child = nodeService.createNode(guestHome, ContentModel.ASSOC_CONTAINS, QName.createQName(guid), ContentModel.TYPE_FOLDER);
               NodeRef testRootNode = child.getChildRef();
               nodeService.setProperty(testRootNode , ContentModel.PROP_TITLE, guid);   
               nodeService.setProperty(testRootNode , ContentModel.PROP_NAME, guid);
               nodes.add(testRootNode);
            
               child = nodeService.createNode(testRootNode, ContentModel.ASSOC_CONTAINS, QName.createQName("testNodeAC"), ContentModel.TYPE_CONTENT);
               NodeRef nodeAC = child.getChildRef();
               nodeService.setProperty(nodeAC , ContentModel.PROP_TITLE, CONTENT_TITLE + "AC");   
                nodeService.setProperty(nodeAC , ContentModel.PROP_NAME, "DemoNodeAC");
            
                {
                    ContentWriter writer = contentService.getWriter(nodeAC , ContentModel.PROP_CONTENT, true);
                    writer.setLocale(CONTENT_LOCALE);
                    writer.putContent(CONTENT_STRING);
                    nodes.add(nodeAC);
                }
            
                child = nodeService.createNode(testRootNode, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,"testNodeA"), ContentModel.TYPE_FOLDER);
                ctx.nodeA = child.getChildRef();
                nodeService.setProperty(ctx.nodeA , ContentModel.PROP_TITLE, "TestNodeA");   
                nodeService.setProperty(ctx.nodeA , ContentModel.PROP_NAME, "TestNodeA");
                nodes.add(ctx.nodeA);
            
                child = nodeService.createNode(testRootNode, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,"testNodeB"), ContentModel.TYPE_FOLDER);
                ctx.nodeB = child.getChildRef();
                nodeService.setProperty(ctx.nodeB , ContentModel.PROP_TITLE, "TestNodeB");   
                nodeService.setProperty(ctx.nodeB , ContentModel.PROP_NAME, "TestNodeB");
                nodes.add(ctx.nodeB);
            
                child = nodeService.createNode(ctx.nodeA, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,"testNodeAA"), ContentModel.TYPE_FOLDER);
                ctx.nodeAA = child.getChildRef();
                nodeService.setProperty(ctx.nodeAA , ContentModel.PROP_TITLE, CONTENT_TITLE);   
                nodeService.setProperty(ctx.nodeAA , ContentModel.PROP_NAME, "DemoNodeAA" );
                nodes.add(ctx.nodeAA);
            
                child = nodeService.createNode(ctx.nodeA, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,"testNodeAB"), ContentModel.TYPE_FOLDER);
                ctx.nodeAB = child.getChildRef();
                nodeService.setProperty(ctx.nodeAB , ContentModel.PROP_TITLE, CONTENT_TITLE);   
                nodeService.setProperty(ctx.nodeAB , ContentModel.PROP_NAME, "DemoNodeAB" );
                nodes.add(ctx.nodeAB);
            
                child = nodeService.createNode(ctx.nodeAB, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,"testNodeABA"), ContentModel.TYPE_FOLDER);
                ctx.nodeABA = child.getChildRef();
                nodeService.setProperty(ctx.nodeABA , ContentModel.PROP_TITLE, CONTENT_TITLE);   
                nodeService.setProperty(ctx.nodeABA , ContentModel.PROP_NAME, "DemoNodeABA" );
                nodes.add(ctx.nodeABA);
            
                child = nodeService.createNode(ctx.nodeAB, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,"testNodeABB"), ContentModel.TYPE_FOLDER);
                ctx.nodeABB = child.getChildRef();
                nodeService.setProperty(ctx.nodeABB , ContentModel.PROP_TITLE, CONTENT_TITLE);   
                nodeService.setProperty(ctx.nodeABB , ContentModel.PROP_NAME, "DemoNodeABB" );
                nodes.add(ctx.nodeABB);
            
                child = nodeService.createNode(ctx.nodeAB, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,"testNodeABC"), ContentModel.TYPE_FOLDER);
                ctx.nodeABC = child.getChildRef();
                nodeService.setProperty(ctx.nodeABC , ContentModel.PROP_TITLE, CONTENT_TITLE);   
                nodeService.setProperty(ctx.nodeABC , ContentModel.PROP_NAME, "DemoNodeABC" );
                nodes.add(ctx.nodeABC);
                
                /**
                 * Now go ahead and create our first transfer target
                 */
                if(!transferService.targetExists(targetName))
                {
                    ctx.transferMe = createTransferTarget(targetName);
                }
                else
                {
                    ctx.transferMe = transferService.getTransferTarget(targetName);
                }       
                
                return ctx;
            } 
        };
        
        final TestContext testContext = tran.doInTransaction(setupCB); 
        
        RetryingTransactionCallback<Void> transferCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                TransferDefinition definition = new TransferDefinition();
                definition.setNodes(nodes);
                transferService.transfer(targetName, definition);
              
               return null;
            }
        };
        
        tran.doInTransaction(transferCB); 
        
        RetryingTransactionCallback<Void> checkTransferCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                NodeRef destNodeA;
                NodeRef destNodeB;
                NodeRef destNodeAA;
                NodeRef destNodeAB;
                NodeRef destNodeABA;
                NodeRef destNodeABB;
                NodeRef destNodeABC;

                // Now validate that the target node exists and has similar properties to the source
                destNodeA = testNodeFactory.getMappedNodeRef(testContext.nodeA);
                assertFalse("unit test stuffed up - comparing with self", destNodeA.equals(testContext.transferMe.getNodeRef()));
                assertTrue("dest node ref A does not exist", nodeService.exists(destNodeA));
                assertEquals("title is wrong", (String)nodeService.getProperty(destNodeA, ContentModel.PROP_TITLE), "TestNodeA"); 
                assertEquals("type is wrong", nodeService.getType(testContext.nodeA), nodeService.getType(destNodeA));
                
                destNodeB = testNodeFactory.getMappedNodeRef(testContext.nodeB);
                assertTrue("dest node B does not exist", nodeService.exists(destNodeB));
                
                destNodeAA = testNodeFactory.getMappedNodeRef(testContext.nodeAA);
                assertTrue("dest node AA ref does not exist", nodeService.exists(destNodeAA));
                
                destNodeAB = testNodeFactory.getMappedNodeRef(testContext.nodeAB);
                assertTrue("dest node AB ref does not exist", nodeService.exists(destNodeAB));
                
                destNodeABA = testNodeFactory.getMappedNodeRef(testContext.nodeABA);
                assertTrue("dest node ABA ref does not exist", nodeService.exists(destNodeABA));
                
                destNodeABB = testNodeFactory.getMappedNodeRef(testContext.nodeABB);
                assertTrue("dest node ABB ref does not exist", nodeService.exists(destNodeABB));
                
                destNodeABC = testNodeFactory.getMappedNodeRef(testContext.nodeABC);
                assertTrue("dest node ABC ref does not exist", nodeService.exists(destNodeABC));
              
               return null;
            }
        };
        
        tran.doInTransaction(checkTransferCB); 
        
        /**
         * Update a single node (NodeAB) from the middle of the tree
         */
        RetryingTransactionCallback<Void> updateSingleNodeCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                nodeService.setProperty(testContext.nodeAB , ContentModel.PROP_TITLE, CONTENT_TITLE_UPDATED);   
                
                TransferDefinition definition = new TransferDefinition();
                Set<NodeRef>toUpdate = new HashSet<NodeRef>();
                toUpdate.add(testContext.nodeAB);
                definition.setNodes(toUpdate);
                transferService.transfer(targetName, definition);
                         
               return null;
            }
        };
          
        tran.doInTransaction(updateSingleNodeCB); 
            
        /**
         * Now generate a large number of nodes
         */
        RetryingTransactionCallback<Void> generateLotsOfNodesCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                for(int i = 0; i < 100; i++)
                {
                    ChildAssociationRef child = nodeService.createNode(testContext.nodeABA, ContentModel.ASSOC_CONTAINS, QName.createQName(GUID.generate() + i), ContentModel.TYPE_CONTENT);
                
                    NodeRef nodeX = child.getChildRef();
                    nodeService.setProperty(nodeX , ContentModel.PROP_TITLE, CONTENT_TITLE + i);   
                    nodeService.setProperty(nodeX , ContentModel.PROP_NAME, CONTENT_NAME +i);
                    nodes.add(nodeX);
                    
                    ContentWriter writer = contentService.getWriter(nodeX, ContentModel.PROP_CONTENT, true);
                    writer.setLocale(CONTENT_LOCALE);
                    writer.putContent(CONTENT_STRING + i);
                }
      
               return null;
            }
        };
          
        tran.doInTransaction(generateLotsOfNodesCB); 
        
        /**
         * Transfer our transfer target nodes 
         */
        tran.doInTransaction(transferCB); 
      
    } // end many nodes
    
    /**
     * Test the path based update.
     * 
     * This is a unit test so it does some shenanigans to send to the same instance of alfresco.
     */
    @Test
    public void testPathBasedUpdate() throws Exception
    {
        final RetryingTransactionHelper tran = transactionService.getRetryingTransactionHelper();

        final String CONTENT_TITLE = "ContentTitle";
        final String CONTENT_TITLE_UPDATED = "ContentTitleUpdated";
        final String CONTENT_NAME = GUID.generate();
        final Locale CONTENT_LOCALE = Locale.GERMAN; 
        final String CONTENT_STRING = "Hello";
        final QName TEST_QNAME = QName.createQName(CONTENT_NAME);

        class TestContext
        {
            String targetName;
            NodeRef contentNodeRef;
            NodeRef newContentNodeRef;          
            NodeRef guestHome;
            ChildAssociationRef child;
        };

        RetryingTransactionCallback<TestContext> setupCB = new RetryingTransactionCallback<TestContext>()
        {
            @Override
            public TestContext execute() throws Throwable
            {
                TestContext ctx = new TestContext();
                ctx.targetName = GUID.generate();
                ctx.guestHome = repositoryHelper.getGuestHome();

                /**
                 * Create a test node that we will transfer.   Its path is what is important
                 */
                ctx.child = nodeService.createNode(ctx.guestHome, ContentModel.ASSOC_CONTAINS, TEST_QNAME, ContentModel.TYPE_CONTENT);
                ctx.contentNodeRef = ctx.child.getChildRef();
                nodeService.setProperty(ctx.contentNodeRef, ContentModel.PROP_TITLE, CONTENT_TITLE);   
                nodeService.setProperty(ctx.contentNodeRef, ContentModel.PROP_NAME, CONTENT_NAME);

                ContentWriter writer = contentService.getWriter(ctx.contentNodeRef, ContentModel.PROP_CONTENT, true);
                writer.setLocale(CONTENT_LOCALE);
                writer.putContent(CONTENT_STRING);

                /**
                 * Now go ahead and create our first transfer target
                 */
                if(!transferService.targetExists(ctx.targetName))
                {
                    createTransferTarget(ctx.targetName);
                }
                else
                {
                    transferService.getTransferTarget(ctx.targetName);
                }              

                return ctx;
            } 
        };

        final TestContext testContext = tran.doInTransaction(setupCB);

        /**
         *  For unit test 
         *  - replace the HTTP transport with the in-process transport
         *  - replace the node factory with one that will map node refs, paths etc.
         */
        TransferTransmitter transmitter = new UnitTestInProcessTransmitterImpl(this.receiver, this.contentService, transactionService);
        transferServiceImpl.setTransmitter(transmitter);
        final UnitTestTransferManifestNodeFactory testNodeFactory = new UnitTestTransferManifestNodeFactory(this.transferManifestNodeFactory); 
        transferServiceImpl.setTransferManifestNodeFactory(testNodeFactory); 
        List<Pair<Path, Path>> pathMap = testNodeFactory.getPathMap();
        // Map company_home/guest_home to company_home so tranferred nodes and moved "up" one level.
        pathMap.add(new Pair<Path, Path>(PathHelper.stringToPath(GUEST_HOME_XPATH_QUERY), PathHelper.stringToPath(COMPANY_HOME_XPATH_QUERY)));

        DescriptorService mockedDescriptorService = getMockDescriptorService(REPO_ID_A);
        transferServiceImpl.setDescriptorService(mockedDescriptorService);

        RetryingTransactionCallback<Void> step1CB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                /**
                 * Transfer our transfer target node
                 */     
                TransferDefinition definition = new TransferDefinition();
                Set<NodeRef>nodes = new HashSet<NodeRef>();
                nodes.add(testContext.contentNodeRef);
                definition.setNodes(nodes);
                transferService.transfer(testContext.targetName, definition);
                return null;
            }
        };
        tran.doInTransaction(step1CB);

        RetryingTransactionCallback<Void> transfer1CB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                // Now validate that the target node exists and has similar properties to the source
                NodeRef destNodeRef = testNodeFactory.getMappedNodeRef(testContext.contentNodeRef);
                assertFalse("unit test stuffed up - comparing with self", destNodeRef.equals(testContext.contentNodeRef));
                assertTrue("dest node ref does not exist", nodeService.exists(destNodeRef));
                assertEquals("title is wrong", (String)nodeService.getProperty(destNodeRef, ContentModel.PROP_TITLE), CONTENT_TITLE); 
                assertEquals("type is wrong", nodeService.getType(testContext.contentNodeRef), nodeService.getType(destNodeRef));

                /**
                 * Now delete the content node and re-create another one with the old path
                 */
                nodeService.deleteNode(testContext.contentNodeRef);

                testContext.child = nodeService.createNode(testContext.guestHome, ContentModel.ASSOC_CONTAINS, TEST_QNAME, ContentModel.TYPE_CONTENT);
                testContext.newContentNodeRef = testContext.child.getChildRef();
                nodeService.setProperty(testContext.newContentNodeRef, ContentModel.PROP_TITLE, CONTENT_TITLE_UPDATED); 
                return null;
            }
        };
        tran.doInTransaction(transfer1CB);

        RetryingTransactionCallback<Void> transfer1CB2 = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                /**
                 * Transfer our node which is a new node (so will not exist on the back end) with a path that already has a node.
                 */
                TransferDefinition definition = new TransferDefinition();
                Set<NodeRef>nodes = new HashSet<NodeRef>();
                nodes.add(testContext.newContentNodeRef);
                definition.setNodes(nodes);
                transferService.transfer(testContext.targetName, definition);
                return null;
            }
        };
        tran.doInTransaction(transfer1CB2);

        RetryingTransactionCallback<Void> validateStep1CB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                NodeRef oldDestNodeRef = testNodeFactory.getMappedNodeRef(testContext.contentNodeRef);
                NodeRef newDestNodeRef = testNodeFactory.getMappedNodeRef(testContext.newContentNodeRef);

                // Now validate that the target node does not exist - it should have 
                // been updated by path.
                assertFalse("unit test stuffed up - comparing with self", oldDestNodeRef.equals(newDestNodeRef));
                assertFalse("new dest node ref exists", nodeService.exists(newDestNodeRef));
                assertTrue("old dest node does not exists", nodeService.exists(oldDestNodeRef));

                assertEquals("title is wrong", (String)nodeService.getProperty(oldDestNodeRef, ContentModel.PROP_TITLE), CONTENT_TITLE_UPDATED); 
                //             assertEquals("type is wrong", nodeService.getType(contentNodeRef), nodeService.getType(destNodeRef));        
                return null;
            }
        };
        tran.doInTransaction(validateStep1CB);

    } // Path based update

    
    /**
     * Test the transfer method when it is running async.
     * 
     * This is a unit test so it does some shenanigans to send to the same instance of alfresco.
     */
    @Test
    public void testAsyncCallback() throws Exception
    {
        final int MAX_SLEEPS = 5;
       
        final RetryingTransactionHelper tran = transactionService.getRetryingTransactionHelper();
        
        /**
         * Unit test kludge to transfer from guest home to company home
         */
        final UnitTestTransferManifestNodeFactory testNodeFactory = unitTestKludgeToTransferGuestHomeToCompanyHome();

        /**
          * This needs to be committed before we can call transfer asycnc.
          */
        final String CONTENT_TITLE = "ContentTitle";
        final String CONTENT_NAME_A = "Demo Node A";
        final String CONTENT_NAME_B = "Demo Node B";
        final Locale CONTENT_LOCALE = Locale.GERMAN; 
        final String CONTENT_STRING = "Hello";
        
        final String targetName = "testAsyncCallback";
        class TestContext
        {
            TransferTarget transferMe;
            NodeRef nodeRefA = null;
            NodeRef nodeRefB = null;
        };
        
        RetryingTransactionCallback<TestContext> setupCB = new RetryingTransactionCallback<TestContext>()
        {
            @Override
            public TestContext execute() throws Throwable
            {
                TestContext ctx = new TestContext();
                
               final NodeRef guestHome = repositoryHelper.getGuestHome();
                
                ctx.nodeRefA = nodeService.getChildByName(guestHome, ContentModel.ASSOC_CONTAINS, CONTENT_NAME_A);
                
                if(ctx.nodeRefA == null)
                {
                    /**
                     * Create a test node that we will read and write
                     */
                    ChildAssociationRef child = nodeService.createNode(guestHome, ContentModel.ASSOC_CONTAINS, QName.createQName(GUID.generate()), ContentModel.TYPE_CONTENT);
                    ctx.nodeRefA = child.getChildRef();
                    nodeService.setProperty(ctx.nodeRefA, ContentModel.PROP_TITLE, CONTENT_TITLE);   
                    nodeService.setProperty(ctx.nodeRefA, ContentModel.PROP_NAME, CONTENT_NAME_A);
            
                    ContentWriter writer = contentService.getWriter(ctx.nodeRefA, ContentModel.PROP_CONTENT, true);
                    writer.setLocale(CONTENT_LOCALE);
                    writer.putContent(CONTENT_STRING);
                }
                
                ctx.nodeRefB = nodeService.getChildByName(guestHome, ContentModel.ASSOC_CONTAINS, CONTENT_NAME_B);
                
                if(ctx.nodeRefB == null)
                {
                    ChildAssociationRef  child = nodeService.createNode(guestHome, ContentModel.ASSOC_CONTAINS, QName.createQName(GUID.generate()), ContentModel.TYPE_CONTENT);
                    ctx.nodeRefB = child.getChildRef();
                    nodeService.setProperty(ctx.nodeRefB, ContentModel.PROP_TITLE, CONTENT_TITLE);   
                    nodeService.setProperty(ctx.nodeRefB, ContentModel.PROP_NAME, CONTENT_NAME_B);
            
                    ContentWriter writer = contentService.getWriter(ctx.nodeRefB, ContentModel.PROP_CONTENT, true);
                    writer.setLocale(CONTENT_LOCALE);
                    writer.putContent(CONTENT_STRING);
                }
                
                /**
                 * Now go ahead and create our first transfer target
                 */
                if(!transferService.targetExists(targetName))
                {
                    createTransferTarget(targetName);
                }
                else
                {
                    transferService.getTransferTarget(targetName);
                }           

                return ctx;
            } 
        };
        
        final TestContext testContext = tran.doInTransaction(setupCB); 
    
        RetryingTransactionCallback<List<TransferEvent>> transferCB = new RetryingTransactionCallback<List<TransferEvent>>() {

            @Override
            public List<TransferEvent> execute() throws Throwable
            {
                List<TransferEvent>transferReport = new ArrayList<TransferEvent>(50);
                
                TestTransferCallback callback = new TestTransferCallback();
                Set<TransferCallback> callbacks = new HashSet<TransferCallback>();
                callbacks.add(callback);
                TransferDefinition definition = new TransferDefinition();
                Set<NodeRef>nodes = new HashSet<NodeRef>();
                nodes.add(testContext.nodeRefA);
                nodes.add(testContext.nodeRefB);
                definition.setNodes(nodes);
                
                transferService.transferAsync(targetName, definition, callbacks);
                logger.debug("transfer async has returned");
                
                /**
                 * Need to poll the transfer events here until callback receives the last event
                 */
                Queue<TransferEvent> events = callback.getEvents();
                
                int sleepCount = MAX_SLEEPS;
                boolean ended = false;
                
                TransferEvent event = events.poll();
                while (!ended)
                {
                    logger.debug("polling loop:" + sleepCount);
                    
                    while(event != null)
                    {
                        /**
                         * Got an event - reset the sleep counter
                         */
                        sleepCount = MAX_SLEEPS;
                                                
                        logger.debug("Got an event" + event.toString());
                        
                        /**
                         * Squirrel away the event for analysis later
                         */
                        transferReport.add(event);
                        
                        /**
                         * If we read the last record which will either be SUCCESS or ERROR then we we have finished
                         */
                        if(event.isLast())
                        {
                            logger.debug("got last event");
                            ended = true;                            
                        }
                        
                        /**
                         * Try to get the next event
                         */
                        event = events.poll();
                    }
                    
                    if(event == null && !ended)
                    {
                        if(sleepCount <= 0)
                        {
                            fail("timed out without receiving last event");
                            ended = true;
                        }
                        else
                        {
                            /**
                             *  No content - go to sleep to wait for some more
                             */
                            if(sleepCount-- > 0)
                            {
                                // Sleep for 5 second
                                Thread.sleep(5000);
                            }                
                        }
                        
                        /**
                         * Try to get the next event
                         */
                        event = events.poll();
                    }
                }
            
                return transferReport;
            }
        };
        
        /**
         * The transfer report is a plain report of the transfer - no async shenanigans to worry about
         */
        final List<TransferEvent>transferReport = tran.doInTransaction(transferCB); 
                     
        /**
          * Now validate the transferReport
          */
        assertTrue("transfer report is too small", transferReport.size() > 2);
        assertTrue("transfer report does not start with START", transferReport.get(0).getTransferState().equals(TransferEvent.TransferState.START));
            
        boolean success = false;
        for(TransferEvent event : transferReport)
        {
           if(event.getTransferState() == TransferEvent.TransferState.SUCCESS)
           {
               success = true;
           }
        } 
        assertTrue("transfer report does not contain SUCCESS", success);

    } // test async callback
    
    
    /**
     * Test the transfer cancel method when it is running async.
     * 
     * This is a unit test so it does some shenanigans to send to the same instance of alfresco.
     */
    @Test
    public void testAsyncCancel() throws Exception
    {
        final int MAX_SLEEPS = 5;

        final String CONTENT_TITLE = "ContentTitle";
        final String CONTENT_NAME_A = "Demo Node A";
        final String CONTENT_NAME_B = "Demo Node B";
        final Locale CONTENT_LOCALE = Locale.GERMAN; 
        final String CONTENT_STRING = "Hello";
        final String targetName = "testAsyncCallback";
        final NodeRef guestHome = repositoryHelper.getGuestHome();

//        final RetryingTransactionHelper tran = transactionService.getRetryingTransactionHelper();

        class TestContext
        {
            TransferTarget transferMe;
            NodeRef nodeRefA = null;
            NodeRef nodeRefB = null;
        };

        /**
         * Unit test kludge to transfer from guest home to company home
         */
        final UnitTestTransferManifestNodeFactory testNodeFactory = unitTestKludgeToTransferGuestHomeToCompanyHome();


        RetryingTransactionCallback<TestContext> setupCB = new RetryingTransactionCallback<TestContext>()
        {
            @Override
            public TestContext execute() throws Throwable
            {
                TestContext ctx = new TestContext();

                ctx.nodeRefA = nodeService.getChildByName(guestHome, ContentModel.ASSOC_CONTAINS, CONTENT_NAME_A);

                if(ctx.nodeRefA == null)
                {
                    /**
                     * Create a test node that we will read and write
                     */
                    ChildAssociationRef child = nodeService.createNode(guestHome, ContentModel.ASSOC_CONTAINS, QName.createQName(GUID.generate()), ContentModel.TYPE_CONTENT);
                    ctx.nodeRefA = child.getChildRef();
                    nodeService.setProperty(ctx.nodeRefA, ContentModel.PROP_TITLE, CONTENT_TITLE);   
                    nodeService.setProperty(ctx.nodeRefA, ContentModel.PROP_NAME, CONTENT_NAME_A);

                    ContentWriter writer = contentService.getWriter(ctx.nodeRefA, ContentModel.PROP_CONTENT, true);
                    writer.setLocale(CONTENT_LOCALE);
                    writer.putContent(CONTENT_STRING);
                }

                ctx.nodeRefB = nodeService.getChildByName(guestHome, ContentModel.ASSOC_CONTAINS, CONTENT_NAME_B);

                if(ctx.nodeRefB == null)
                {
                    ChildAssociationRef  child = nodeService.createNode(guestHome, ContentModel.ASSOC_CONTAINS, QName.createQName(GUID.generate()), ContentModel.TYPE_CONTENT);
                    ctx.nodeRefB = child.getChildRef();
                    nodeService.setProperty(ctx.nodeRefB, ContentModel.PROP_TITLE, CONTENT_TITLE);   
                    nodeService.setProperty(ctx.nodeRefB, ContentModel.PROP_NAME, CONTENT_NAME_B);

                    ContentWriter writer = contentService.getWriter(ctx.nodeRefB, ContentModel.PROP_CONTENT, true);
                    writer.setLocale(CONTENT_LOCALE);
                    writer.putContent(CONTENT_STRING);
                }

                /**
                 * Now go ahead and create our first transfer target
                 */
                if(!transferService.targetExists(targetName))
                {
                    createTransferTarget(targetName);
                }
                else
                {
                    transferService.getTransferTarget(targetName);
                }   

                return ctx;
            } 
        };

        final TestContext testContext = transactionService.getRetryingTransactionHelper().doInTransaction(setupCB, false, true);

        /**
         * The transfer report is a plain report of the transfer - no async shenanigans to worry about
         */
        final List<TransferEvent>transferReport = new ArrayList<TransferEvent>(50);

        RetryingTransactionCallback<Void> transferAsyncCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {

                /**
                 * The poison callback will cancel the transfer after 
                 * the begin
                 */
                TransferCallback poison = new TransferCallback() 
                {
                    String transferId = null;

                    public void processEvent(TransferEvent event)
                    {
                        logger.debug(event.toString());

                        if(event instanceof TransferEventBegin)
                        {
                            TransferEventBegin beginEvent = (TransferEventBegin)event;
                            transferId = beginEvent.getTransferId();

                            transferService.cancelAsync(transferId);
                        }
                    }
                };

                TestTransferCallback callback = new TestTransferCallback();
                Set<TransferCallback> callbacks = new HashSet<TransferCallback>();
                callbacks.add(callback);
                callbacks.add(poison);
                TransferDefinition definition = new TransferDefinition();
                Set<NodeRef>nodes = new HashSet<NodeRef>();
                nodes.add(testContext.nodeRefA);
                nodes.add(testContext.nodeRefB);
                definition.setNodes(nodes);

                transferService.transferAsync(targetName, definition, callbacks);
                logger.debug("transfer async has returned");

                /**
                 * Need to poll the transfer events here until callback receives the last event
                 */
                Queue<TransferEvent> events = callback.getEvents();

                int sleepCount = MAX_SLEEPS;
                boolean ended = false;

                TransferEvent event = events.poll();
                while (!ended)
                {
                    logger.debug("polling loop:" + sleepCount);

                    while(event != null)
                    {
                        /**
                         * Got an event - reset the sleep counter
                         */
                        sleepCount = MAX_SLEEPS;

                        logger.debug("Got an event" + event.toString());

                        /**
                         * Squirrel away the event for analysis later
                         */
                        transferReport.add(event);

                        /**
                         * If we read the last record which will either be SUCCESS or ERROR then we we have finished
                         */
                        if(event.isLast())
                        {
                            logger.debug("got last event");
                            ended = true;                            
                        }

                        /**
                         * Try to get the next event
                         */
                        event = events.poll();
                    }

                    if(event == null && !ended)
                    {
                        if(sleepCount <= 0)
                        {
                            fail("timed out without receiving last event");
                            ended = true;
                        }
                        else
                        {
                            /**
                             *  No content - go to sleep to wait for some more
                             */
                            if(sleepCount-- > 0)
                            {
                                // Sleep for 5 second
                                Thread.sleep(5000);
                            }                
                        }

                        /**
                         * Try to get the next event
                         */
                        event = events.poll();
                    }
                }



                return null;
            }
        };
        transactionService.getRetryingTransactionHelper().doInTransaction(transferAsyncCB);


        /**
         * Now validate the transferReport
         */
        assertTrue("transfer report is too small", transferReport.size() > 3);
        assertTrue("transfer report does not start with START", transferReport.get(0).getTransferState().equals(TransferEvent.TransferState.START));
        assertTrue("transfer report does not end with CANCELLED", transferReport.get(transferReport.size()-1).getTransferState().equals(TransferEvent.TransferState.CANCELLED));
        // last event is the transfer report event.


    } // test async cancel

    private void dumpToSystemOut(NodeRef nodeRef) throws IOException
    {
        ContentReader reader2 = contentService.getReader(nodeRef, ContentModel.PROP_CONTENT);
        assertNotNull("transfer reader is null", reader2);
        InputStream is = reader2.getContentInputStream();
    
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
    
        String s = br.readLine();
        while(s != null)
        {
            System.out.println(s);   
            s = br.readLine();
        }
    }

    private  UnitTestTransferManifestNodeFactory unitTestKludgeToTransferGuestHomeToCompanyHome()
    {
        /**
         *  For unit test 
         *  - replace the HTTP transport with the in-process transport
         *  - replace the node factory with one that will map node refs, paths etc.
         */
        TransferTransmitter transmitter = new UnitTestInProcessTransmitterImpl(this.receiver, this.contentService, transactionService);
        transferServiceImpl.setTransmitter(transmitter);
        UnitTestTransferManifestNodeFactory testNodeFactory = new UnitTestTransferManifestNodeFactory(this.transferManifestNodeFactory); 
        transferServiceImpl.setTransferManifestNodeFactory(testNodeFactory); 
        List<Pair<Path, Path>> pathMap = testNodeFactory.getPathMap();
        // Map company_home/guest_home to company_home so tranferred nodes and moved "up" one level.
        pathMap.add(new Pair<Path, Path>(PathHelper.stringToPath(GUEST_HOME_XPATH_QUERY), PathHelper.stringToPath(COMPANY_HOME_XPATH_QUERY)));
   
        DescriptorService mockedDescriptorService = getMockDescriptorService(REPO_ID_A);
        transferServiceImpl.setDescriptorService(mockedDescriptorService);
        
        return testNodeFactory;
    }
    
    /**
     * Test the transfer method with regard to big content. 
     * 
     * This test takes a long time to run and is by default not run in the overnight build.
     * 
     * Turn it on by turning debug logging on for this class or by changing the "runTest" value;
     */
    @Test
    public void testTransferOneNodeWithBigContent() throws Exception
    { 
        /**
         * This test takes a long time to run - so switch it on and off here.
         */
        boolean runTest = false;
        if(runTest || logger.isDebugEnabled())
        {
            final String CONTENT_TITLE = "ContentTitle";
            final String CONTENT_NAME = "BigContent";
            final Locale CONTENT_LOCALE = Locale.UK; 
        
            logger.debug("testTransferOneNodeWithBigContent starting");
            
            final RetryingTransactionHelper tran = transactionService.getRetryingTransactionHelper();
            
            /**
             * Unit test kludge to transfer from guest home to company home
             */
            final UnitTestTransferManifestNodeFactory testNodeFactory = unitTestKludgeToTransferGuestHomeToCompanyHome();
                
            final String targetName = "testTransferOneNodeWithBigContent";
        
            class TestContext
            {
                TransferTarget transferMe;
                NodeRef contentNodeRef;
                NodeRef destNodeRef;
            };
            
            RetryingTransactionCallback<TestContext> setupCB = new RetryingTransactionCallback<TestContext>()
            {
                @Override
                public TestContext execute() throws Throwable
                {
                    TestContext ctx = new TestContext();
                    
                    NodeRef guestHome = repositoryHelper.getGuestHome();
              
                    ctx.contentNodeRef = nodeService.getChildByName(guestHome, ContentModel.ASSOC_CONTAINS, CONTENT_NAME);
                    if(ctx.contentNodeRef == null)
                    {
                        /**
                         * Create a test node that we will read and write
                         */
                        ChildAssociationRef child = nodeService.createNode(guestHome, ContentModel.ASSOC_CONTAINS, QName.createQName(CONTENT_NAME), ContentModel.TYPE_CONTENT);
            
                        File tempFile = TempFileProvider.createTempFile("test", ".dat");
                        FileWriter fw = new FileWriter(tempFile);
                        for(int i = 0; i < 100000000; i++)
                        {
                            fw.write("hello world this is my text, I wonder how much text I can transfer?" + i);
                        }
                        System.out.println("Temp File Size is:" + tempFile.length());
                        fw.close();
     
                        ctx.contentNodeRef = child.getChildRef();
                        ContentWriter writer = contentService.getWriter(ctx.contentNodeRef, ContentModel.PROP_CONTENT, true);
                        writer.setLocale(CONTENT_LOCALE);
                        writer.setMimetype("application/data");
                        writer.putContent(tempFile);
            
                        tempFile.delete();
            
                        nodeService.setProperty(ctx.contentNodeRef, ContentModel.PROP_TITLE, CONTENT_TITLE);   
                        nodeService.setProperty(ctx.contentNodeRef, ContentModel.PROP_NAME, CONTENT_NAME);
                    }
                    if(!transferService.targetExists(targetName))
                    {
                        createTransferTarget(targetName);
                    }
                    
                    return ctx;
                } 
            };
            
            final TestContext testContext = tran.doInTransaction(setupCB); 
        
            RetryingTransactionCallback<Void> transferCB = new RetryingTransactionCallback<Void>() {

                @Override
                public Void execute() throws Throwable
                {
                    TransferDefinition definition = new TransferDefinition();
                    Set<NodeRef>nodes = new HashSet<NodeRef>();
                    nodes.add(testContext.contentNodeRef);
                    definition.setNodes(nodes);
                    transferService.transfer(targetName, definition);
                    
                    return null;
                }
            };
            
            RetryingTransactionCallback<Void> finishCB = new RetryingTransactionCallback<Void>() {

                @Override
                public Void execute() throws Throwable
                {
                    NodeRef oldDestNodeRef = testNodeFactory.getMappedNodeRef(testContext.contentNodeRef);
                    
                    ContentReader source = contentService.getReader(testContext.contentNodeRef, ContentModel.PROP_CONTENT);
                    ContentReader destination = contentService.getReader(oldDestNodeRef, ContentModel.PROP_CONTENT);
                    
                    assertNotNull("source is null", source);
                    assertNotNull("destination is null", destination);
                    assertEquals("size different", source.getSize(), destination.getSize());
                    
                    /**
                     * Now get rid of the transferred node so that the test can run again. 
                     */
                    nodeService.deleteNode(oldDestNodeRef);
                    
                    return null;
                }
            };
            
            /**
             * This is the test
             */
            tran.doInTransaction(transferCB); 
            tran.doInTransaction(finishCB); 
        }
        else
        {
            System.out.println("test supressed");
        }
    } // test big content     

    /**  
     * Test the transfer method with regard to an empty content property.  ALF-4865
     * 
     * Step 1: create a node with an empty content property
     * transfer
     * 
     * Step 2: add non empty content property 
     * transfer
     * 
     * Step 3: update from non empty content to empty content property
     * transfer
     * 
     * This is a unit test so it does some shenanigans to send to the same instance of alfresco.
     */
    @Test
    public void testEmptyContent() throws Exception
    {
        final RetryingTransactionHelper tran = transactionService.getRetryingTransactionHelper();

        final String CONTENT_TITLE = "ContentTitle";
        final String CONTENT_TITLE_UPDATED = "ContentTitleUpdated";
        final Locale CONTENT_LOCALE = Locale.ENGLISH; 
        final String CONTENT_ENCODING = "UTF-8";
        final String CONTENT_STRING = "The quick brown fox jumps over the lazy dog.";
        final String targetName = "testTransferEmptyContent";

        /**
         *  For unit test 
         *  - replace the HTTP transport with the in-process transport
         *  - replace the node factory with one that will map node refs, paths etc.
         *  
         *  Fake Repository Id
         */
        class TestContext
        {
            TransferTarget transferMe;
            NodeRef contentNodeRef;
            NodeRef savedDestinationNodeRef;
        };

        /**
         * Unit test kludge to transfer from guest home to company home
         */
        final UnitTestTransferManifestNodeFactory testNodeFactory = unitTestKludgeToTransferGuestHomeToCompanyHome();

        DescriptorService mockedDescriptorService = getMockDescriptorService(REPO_ID_A);
        transferServiceImpl.setDescriptorService(mockedDescriptorService);

        TransferTarget transferMe;

        RetryingTransactionCallback<TestContext> setupCB = new RetryingTransactionCallback<TestContext>()
        {
            @Override
            public TestContext execute() throws Throwable
            {
                TestContext ctx = new TestContext();

                NodeRef guestHome = repositoryHelper.getGuestHome();

                /**
                 * Create a test node with an empty content that we will read and write
                 */       
                String name = GUID.generate();
                ChildAssociationRef child = nodeService.createNode(guestHome, ContentModel.ASSOC_CONTAINS, QName.createQName(name), ContentModel.TYPE_CONTENT);
                ctx.contentNodeRef = child.getChildRef();
                nodeService.setProperty(ctx.contentNodeRef, ContentModel.PROP_TITLE, CONTENT_TITLE);   
                nodeService.setProperty(ctx.contentNodeRef, ContentModel.PROP_NAME, name);
                ContentData cd = new ContentData(null, null, 0, null);
                nodeService.setProperty(ctx.contentNodeRef, ContentModel.PROP_CONTENT, cd);

                if(!transferService.targetExists(targetName))
                {
                    ctx.transferMe = createTransferTarget(targetName);
                }
                else
                {
                    ctx.transferMe = transferService.getTransferTarget(targetName);
                }
                transferService.enableTransferTarget(targetName, true);

                return ctx;
            } 
        };
        final TestContext testContext = tran.doInTransaction(setupCB); 

        final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");  

        /**
         * Step 1: Transfer our node which has empty content
         */
        logger.debug("testEmptyContent : First transfer - create new node (empty content)");


        RetryingTransactionCallback<Void> step1CB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                ContentReader reader = contentService.getReader(testContext.contentNodeRef, ContentModel.PROP_CONTENT);
                assertNull("test setup content reader not null", reader);
                Map<QName, Serializable> props = nodeService.getProperties(testContext.contentNodeRef);
                assertTrue(props.containsKey(ContentModel.PROP_CONTENT));

                TransferDefinition definition = new TransferDefinition();
                Set<NodeRef>nodes = new HashSet<NodeRef>();
                nodes.add(testContext.contentNodeRef);
                definition.setNodes(nodes);
                transferService.transfer(targetName, definition);

                return null;
            }
        };
        tran.doInTransaction(step1CB);

        RetryingTransactionCallback<Void> validateStep1CB = new RetryingTransactionCallback<Void>() 
        {
            @Override
            public Void execute() throws Throwable
            {
                Serializable modifiedDate = nodeService.getProperty(testContext.contentNodeRef, ContentModel.PROP_MODIFIED);
                if(modifiedDate instanceof Date)
                {
                    logger.debug("srcModified: "  + SDF.format(modifiedDate));
                }

                NodeRef destinationNodeRef = testNodeFactory.getMappedNodeRef(testContext.contentNodeRef);
                testContext.savedDestinationNodeRef = destinationNodeRef;
                assertTrue("content node (dest) does not exist", nodeService.exists(destinationNodeRef));

                ContentReader reader = contentService.getReader(destinationNodeRef, ContentModel.PROP_CONTENT);
                assertNull("content reader not null", reader);
                Map<QName, Serializable> props = nodeService.getProperties(destinationNodeRef);
                assertTrue(props.containsKey(ContentModel.PROP_CONTENT));      
                return null;
            }
        };
        tran.doInTransaction(validateStep1CB);

        /**
         * Step 2: replace empty content with new content
         */
        logger.debug("testEmptyContent : Second transfer - replace empty content with some content");


        RetryingTransactionCallback<Void> step2CB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                Serializable modifiedDate = nodeService.getProperty(testContext.contentNodeRef, ContentModel.PROP_MODIFIED);
                if(modifiedDate instanceof Date)
                {
                    logger.debug("srcModified: "  + SDF.format(modifiedDate));
                }

                ContentWriter writer = contentService.getWriter(testContext.contentNodeRef, ContentModel.PROP_CONTENT, true);
                writer.setLocale(CONTENT_LOCALE);
                writer.setEncoding(CONTENT_ENCODING);
                writer.putContent(CONTENT_STRING);

                return null;
            }
        };

        tran.doInTransaction(step2CB);


        RetryingTransactionCallback<Void> step2TransferCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                ContentReader reader = contentService.getReader(testContext.contentNodeRef, ContentModel.PROP_CONTENT);
                assertNotNull("test setup content reader not null", reader);
                Map<QName, Serializable> props = nodeService.getProperties(testContext.contentNodeRef);
                assertTrue(props.containsKey(ContentModel.PROP_CONTENT));

                /**
                 * Step 2: replace empty content with new content
                 */
                TransferDefinition definition = new TransferDefinition();
                Set<NodeRef>nodes = new HashSet<NodeRef>();
                nodes.add(testContext.contentNodeRef);
                definition.setNodes(nodes);
                transferService.transfer(targetName, definition);

                return null;
            }
        };

        tran.doInTransaction(step2TransferCB);


        RetryingTransactionCallback<Void> step2ValidateCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                NodeRef destinationNodeRef = testNodeFactory.getMappedNodeRef(testContext.contentNodeRef);

                assertEquals("test error destinationNodeRef not correct", testContext.savedDestinationNodeRef, destinationNodeRef);
                ContentReader reader = contentService.getReader(destinationNodeRef, ContentModel.PROP_CONTENT);
                assertNotNull("content reader is null", reader);
                assertTrue("content encoding is wrong", reader.getEncoding().equalsIgnoreCase(CONTENT_ENCODING));
                assertEquals("content locale is wrong", reader.getLocale(), CONTENT_LOCALE);
                assertTrue("content does not exist", reader.exists());
                String contentStr = reader.getContentString();
                assertEquals("Content is wrong", contentStr, CONTENT_STRING);

                return null;
            }
        };

        tran.doInTransaction(step2ValidateCB);

        /**
         * Step 3 - transition from a content property having content to one that is empty
         */
        logger.debug("testEmptyContent : Third transfer - remove existing content");


        RetryingTransactionCallback<Void> step3SetupCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                ContentData cd = new ContentData(null, null, 0, null);
                nodeService.setProperty(testContext.contentNodeRef, ContentModel.PROP_CONTENT, cd);         
                return null;
            }
        };
        tran.doInTransaction(step3SetupCB);


        RetryingTransactionCallback<Void> step3TransferCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                ContentReader reader = contentService.getReader(testContext.contentNodeRef, ContentModel.PROP_CONTENT);
                assertNull("test setup content reader not null", reader);
                Map<QName, Serializable> props = nodeService.getProperties(testContext.contentNodeRef);
                assertTrue(props.containsKey(ContentModel.PROP_CONTENT));

                /**
                 * Step 3: Transfer our node which has empty content to over-write existing
                 * content
                 */
                TransferDefinition definition = new TransferDefinition();
                Set<NodeRef>nodes = new HashSet<NodeRef>();
                nodes.add(testContext.contentNodeRef);
                definition.setNodes(nodes);
                transferService.transfer(targetName, definition);
                return null;
            }
        };
        tran.doInTransaction(step3TransferCB);

        RetryingTransactionCallback<Void> step3ValidateCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                NodeRef destinationNodeRef = testNodeFactory.getMappedNodeRef(testContext.contentNodeRef);
                assertTrue("content node (dest) does not exist", nodeService.exists(destinationNodeRef));

                ContentReader reader = contentService.getReader(destinationNodeRef, ContentModel.PROP_CONTENT);
                assertNull("content reader not null", reader);
                Map<QName, Serializable> props = nodeService.getProperties(destinationNodeRef);
                assertTrue(props.containsKey(ContentModel.PROP_CONTENT));         
                return null;
            }
        };
        tran.doInTransaction(step3ValidateCB);
    } // end of testEmptyContent
    
    
    /**
     * Test the transfer method with regard to a repeated update of content.by sending one node (CRUD).
     * 
     * This is a unit test so it does some shenanigans to send to the same instance of alfresco.
     */
    @Test
    public void testRepeatUpdateOfContent() throws Exception
    {
        final String CONTENT_TITLE = "ContentTitle";
        final Locale CONTENT_LOCALE = Locale.GERMAN; 
        final String CONTENT_ENCODING = "UTF-8";

        /**
         *  For unit test 
         *  - replace the HTTP transport with the in-process transport
         *  - replace the node factory with one that will map node refs, paths etc.
         *  
         *  Fake Repository Id
         */
        final TransferTransmitter transmitter = new UnitTestInProcessTransmitterImpl(receiver, contentService, transactionService);
        transferServiceImpl.setTransmitter(transmitter);
        final UnitTestTransferManifestNodeFactory testNodeFactory = new UnitTestTransferManifestNodeFactory(this.transferManifestNodeFactory); 
        transferServiceImpl.setTransferManifestNodeFactory(testNodeFactory); 
        final List<Pair<Path, Path>> pathMap = testNodeFactory.getPathMap();
        // Map company_home/guest_home to company_home so tranferred nodes and moved "up" one level.
        pathMap.add(new Pair<Path, Path>(PathHelper.stringToPath(GUEST_HOME_XPATH_QUERY), PathHelper.stringToPath(COMPANY_HOME_XPATH_QUERY)));
        
        DescriptorService mockedDescriptorService = getMockDescriptorService(REPO_ID_A);
        transferServiceImpl.setDescriptorService(mockedDescriptorService);
        
        final String targetName = "testRepeatUpdateOfContent";
        
        class TestContext
        {
           TransferTarget transferMe;
           NodeRef contentNodeRef;
           NodeRef destNodeRef;
           String contentString;
        };
       
        RetryingTransactionCallback<TestContext> setupCB = new RetryingTransactionCallback<TestContext>()
        {
            @Override
            public TestContext execute() throws Throwable
            {
                TestContext testContext = new TestContext();
            
               NodeRef guestHome = repositoryHelper.getGuestHome();
   
               /**
                * Create a test node that we will read and write
                */  
               String name = GUID.generate();
               ChildAssociationRef child = nodeService.createNode(guestHome, ContentModel.ASSOC_CONTAINS, QName.createQName(name), ContentModel.TYPE_CONTENT);
               testContext.contentNodeRef = child.getChildRef();
               nodeService.setProperty(testContext.contentNodeRef, ContentModel.PROP_TITLE, CONTENT_TITLE);   
               nodeService.setProperty(testContext.contentNodeRef, ContentModel.PROP_NAME, name);
           
               /**
                * Make sure the transfer target exists and is enabled.
                */
               if(!transferService.targetExists(targetName))
               {
                   testContext.transferMe = createTransferTarget(targetName);
               }
               else
               {
                   testContext.transferMe = transferService.getTransferTarget(targetName);
               }
               transferService.enableTransferTarget(targetName, true);
               return testContext;
            } 
        };
        
        final TestContext testContext = transactionService.getRetryingTransactionHelper().doInTransaction(setupCB, false, true);
        
        RetryingTransactionCallback<Void> updateContentCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                ContentWriter writer = contentService.getWriter(testContext.contentNodeRef, ContentModel.PROP_CONTENT, true);
                writer.setLocale(CONTENT_LOCALE);
                writer.setEncoding(CONTENT_ENCODING);
                writer.putContent(testContext.contentString);
                return null;
            }
        };
        
        RetryingTransactionCallback<Void> transferCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
               TransferDefinition definition = new TransferDefinition();
               Set<NodeRef>nodes = new HashSet<NodeRef>();
               nodes.add(testContext.contentNodeRef);
               definition.setNodes(nodes);
               transferService.transfer(targetName, definition);
               return null;
            }
        };
        
        RetryingTransactionCallback<Void> checkTransferCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                // Now validate that the target node exists and has similar properties to the source
                NodeRef destNodeRef = testNodeFactory.getMappedNodeRef(testContext.contentNodeRef);
           
                ContentReader reader = contentService.getReader(destNodeRef, ContentModel.PROP_CONTENT);
                assertNotNull("content reader is null", reader);
                assertTrue("content encoding is wrong", reader.getEncoding().equalsIgnoreCase(CONTENT_ENCODING));
                assertEquals("content locale is wrong", reader.getLocale(), CONTENT_LOCALE);
                assertTrue("content does not exist", reader.exists());
                String contentStr = reader.getContentString();
                assertEquals("Content is wrong", contentStr, testContext.contentString);
                
                return null;
            }
        };
        
        /**
         * This is the test
         */
        for(int i = 0; i < 6 ; i++)
        {
            logger.debug("testRepeatUpdateContent - iteration:" + i);
            testContext.contentString = String.valueOf(i);
            transactionService.getRetryingTransactionHelper().doInTransaction(updateContentCB);
            transactionService.getRetryingTransactionHelper().doInTransaction(transferCB);
            transactionService.getRetryingTransactionHelper().doInTransaction(checkTransferCB);
        }
    } // test repeat update content

    /**
     * Test the transfer method with regard to replacing a node.  ALF-5109
     * 
     * Step 1: Create a new parent node and child node
     * transfer
     * 
     * Step 2: Delete the parent node
     * transfer
     *  
     * Step 3: Create new parent child node with same names and assocs.
     * transfer
     * 
     * This is a unit test so it does some shenanigans to send to the same instance of alfresco.
     */
    @Test
    public void testReplaceNode() throws Exception
    {
        final RetryingTransactionHelper tran = transactionService.getRetryingTransactionHelper();
        
        final String CONTENT_TITLE = "ContentTitle";
        final String CONTENT_TITLE_UPDATED = "ContentTitleUpdated";
        final Locale CONTENT_LOCALE = Locale.GERMAN; 
        final String CONTENT_STRING = "Hello World";
        final String CONTENT_UPDATE_STRING = "Foo Bar";
        
        /**
         *  For unit test 
         *  - replace the HTTP transport with the in-process transport
         *  - replace the node factory with one that will map node refs, paths etc.
         *  
         *  Fake Repository Id
         */
        TransferTransmitter transmitter = new UnitTestInProcessTransmitterImpl(receiver, contentService, transactionService);
        transferServiceImpl.setTransmitter(transmitter);
        UnitTestTransferManifestNodeFactory testNodeFactory = new UnitTestTransferManifestNodeFactory(this.transferManifestNodeFactory); 
        transferServiceImpl.setTransferManifestNodeFactory(testNodeFactory); 
        List<Pair<Path, Path>> pathMap = testNodeFactory.getPathMap();
        // Map company_home/guest_home to company_home so tranferred nodes and moved "up" one level.
        pathMap.add(new Pair<Path, Path>(PathHelper.stringToPath(GUEST_HOME_XPATH_QUERY), PathHelper.stringToPath(COMPANY_HOME_XPATH_QUERY)));
        
        DescriptorService mockedDescriptorService = getMockDescriptorService(REPO_ID_A);
        transferServiceImpl.setDescriptorService(mockedDescriptorService);

        final NodeRef guestHome = repositoryHelper.getGuestHome();
        
        final String targetName = "testRepeatUpdateOfContent";
        
        class TestContext
        {
           TransferTarget transferMe;
           NodeRef parentNodeRef;
           NodeRef middleNodeRef;
           NodeRef childNodeRef;
           QName parentName;
           QName middleName;
           QName childName;
           
        };
       
        RetryingTransactionCallback<TestContext> setupCB = new RetryingTransactionCallback<TestContext>()
        {
            @Override
            public TestContext execute() throws Throwable
            {
                TestContext testContext = new TestContext();
            
               /**
                * Create a test node that we will read and write
                */  
               String name = GUID.generate();
               
               testContext.parentName = QName.createQName(name);
               testContext.childName = QName.createQName("Ermintrude");
               testContext.middleName = QName.createQName("Matilda");
               
               ChildAssociationRef child = nodeService.createNode(guestHome, ContentModel.ASSOC_CONTAINS, testContext.parentName, ContentModel.TYPE_FOLDER);
               testContext.parentNodeRef = child.getChildRef();
               logger.debug("parentNodeRef created:"  + testContext.parentNodeRef );
               nodeService.setProperty(testContext.parentNodeRef, ContentModel.PROP_TITLE, CONTENT_TITLE);   
               nodeService.setProperty(testContext.parentNodeRef, ContentModel.PROP_NAME, testContext.parentName.getLocalName());
         
               ChildAssociationRef child2 = nodeService.createNode(testContext.parentNodeRef, ContentModel.ASSOC_CONTAINS, testContext.childName, ContentModel.TYPE_FOLDER);
               testContext.middleNodeRef = child2.getChildRef();
               logger.debug("middleNodeRef created:"  + testContext.middleNodeRef );
               nodeService.setProperty(testContext.middleNodeRef, ContentModel.PROP_TITLE, CONTENT_TITLE);   
               nodeService.setProperty(testContext.middleNodeRef, ContentModel.PROP_NAME, testContext.childName.getLocalName());
         
               ChildAssociationRef child3 = nodeService.createNode(testContext.middleNodeRef, ContentModel.ASSOC_CONTAINS, testContext.childName, ContentModel.TYPE_CONTENT);
               testContext.childNodeRef = child3.getChildRef();
               logger.debug("childNodeRef created:"  + testContext.childNodeRef );
               nodeService.setProperty(testContext.childNodeRef, ContentModel.PROP_TITLE, CONTENT_TITLE);   
               nodeService.setProperty(testContext.childNodeRef, ContentModel.PROP_NAME, testContext.childName.getLocalName());
         
               /**
                * Make sure the transfer target exists and is enabled.
                */
               if(!transferService.targetExists(targetName))
               {
                   testContext.transferMe = createTransferTarget(targetName);
               }
               else
               {
                   testContext.transferMe = transferService.getTransferTarget(targetName);
               }
               transferService.enableTransferTarget(targetName, true);
               return testContext;
            } 
        };
        
        final TestContext testContext = tran.doInTransaction(setupCB); 
        
        RetryingTransactionCallback<Void> transferCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
               TransferDefinition definition = new TransferDefinition();
               Collection<NodeRef> nodes = new ArrayList<NodeRef>();
               nodes.add(testContext.childNodeRef);
               nodes.add(testContext.parentNodeRef);
               nodes.add(testContext.middleNodeRef);
               definition.setSync(true);
               definition.setNodes(nodes);
               transferService.transfer(targetName, definition);
               return null;
            }
        };
        
        RetryingTransactionCallback<Void> checkTransferCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {        
                return null;
            }
        };
        
        RetryingTransactionCallback<Void> replaceNodesCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                // Delete the old nodes
                
                nodeService.deleteNode(testContext.middleNodeRef);
                logger.debug("deleted node");
                
                ChildAssociationRef child2 = nodeService.createNode(testContext.parentNodeRef, ContentModel.ASSOC_CONTAINS, testContext.childName, ContentModel.TYPE_FOLDER);
                testContext.middleNodeRef = child2.getChildRef();
                logger.debug("middleNodeRef created:"  + testContext.middleNodeRef );
                nodeService.setProperty(testContext.middleNodeRef, ContentModel.PROP_TITLE, CONTENT_TITLE);   
                nodeService.setProperty(testContext.middleNodeRef, ContentModel.PROP_NAME, testContext.childName.getLocalName());
          
                ChildAssociationRef child3 = nodeService.createNode(testContext.middleNodeRef, ContentModel.ASSOC_CONTAINS, testContext.childName, ContentModel.TYPE_CONTENT);
                testContext.childNodeRef = child3.getChildRef();
                logger.debug("childNodeRef created:"  + testContext.childNodeRef );
                nodeService.setProperty(testContext.childNodeRef, ContentModel.PROP_TITLE, CONTENT_TITLE);   
                nodeService.setProperty(testContext.childNodeRef, ContentModel.PROP_NAME, testContext.childName.getLocalName());
                   
                return null;
            }
        };
        
        // This is the test
        
        tran.doInTransaction(transferCB); 
        tran.doInTransaction(replaceNodesCB);
        tran.doInTransaction(transferCB); 
        tran.doInTransaction(checkTransferCB);         
         
    } // test replace node

//    /**
//     * Test the transfer method with regard to obscure paths.
//     * 
//     * This is a unit test so it does some shenanigans to send to he same instance of alfresco.
//     */
//    public void testHorriblePaths() throws Exception
//    {
//        setDefaultRollback(false);
//        
//        final RetryingTransactionHelper tran = transactionService.getRetryingTransactionHelper();
//        
//        final String CONTENT_TITLE = "ContentTitle";
//        final String CONTENT_TITLE_UPDATED = "ContentTitleUpdated";
//        final String CONTENT_NAME = "Demo Node 1";
//        final Locale CONTENT_LOCALE = Locale.GERMAN; 
//        final String CONTENT_STRING = "The quick brown fox";
//        final Set<NodeRef>nodes = new HashSet<NodeRef>();
//

//        final String targetName = "testManyNodes";
//        
//        class TestContext
//        {
//            TransferTarget transferMe;
//            NodeRef nodeA = null;
//            NodeRef childNode = null;
//        };
//        
//        /**
//         * Unit test kludge to transfer from guest home to company home
//         */
//        final UnitTestTransferManifestNodeFactory testNodeFactory = unitTestKludgeToTransferGuestHomeToCompanyHome();
//        
//        TransferTarget transferMe;
//        
//        final QName[] difficult = { QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,"testNodeB"),
//                QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,"with.dot"),
//                QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,"8332"),
//                QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,"&#~@"),
//                QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,"_-+ )"),
//                QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,"with space"),
//                // A, e with accent
//                QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "\u0041\u00E9"), 
//                // Greek Alpha, Omega
//                QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "\u0391\u03A9")
//        };
//        
//        RetryingTransactionCallback<TestContext> setupCB = new RetryingTransactionCallback<TestContext>()
//        {
//            @Override
//            public TestContext execute() throws Throwable
//            {
//                TestContext ctx = new TestContext();
//                
//                /**
//                 * Get guest home
//                 */
//               String guestHomeQuery = "/app:company_home/app:guest_home";
//               ResultSet guestHomeResult = searchService.query(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, SearchService.LANGUAGE_XPATH, guestHomeQuery);
//               assertEquals("", 1, guestHomeResult.length());
//               NodeRef guestHome = guestHomeResult.getNodeRef(0); 
//       
//               /**
//                * Create a test node that we will read and write
//                */
//               String guid = GUID.generate();
//               
//               /**
//                * Create a tree with "difficult" characters in the path
//                * ManyNodesRoot
//                * A (Folder)
//                * ... childNode
//                */
//               ChildAssociationRef child;
//               
//               child = nodeService.createNode(guestHome, ContentModel.ASSOC_CONTAINS, QName.createQName(guid), ContentModel.TYPE_FOLDER);
//               NodeRef testRootNode = child.getChildRef();
//               nodeService.setProperty(testRootNode , ContentModel.PROP_TITLE, guid);   
//               nodeService.setProperty(testRootNode , ContentModel.PROP_NAME, guid);
//               nodes.add(testRootNode);
//            
//               child = nodeService.createNode(testRootNode, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,"testNodeA"), ContentModel.TYPE_FOLDER);
//               ctx.nodeA = child.getChildRef();
//               nodeService.setProperty(ctx.nodeA , ContentModel.PROP_TITLE, "TestNodeA");   
//               nodeService.setProperty(ctx.nodeA , ContentModel.PROP_NAME, "TestNodeA");
//               nodes.add(ctx.nodeA);
//                
//               NodeRef current = ctx.nodeA;
//                
//               for(QName name : difficult)
//               {
//                   child = nodeService.createNode(current, ContentModel.ASSOC_CONTAINS, name, ContentModel.TYPE_FOLDER);
//                   current = child.getChildRef();
//                   nodeService.setProperty(current , ContentModel.PROP_TITLE, name);   
//                   nodeService.setProperty(current , ContentModel.PROP_NAME, "testName");
//                   nodes.add(current);
//               }
//               
//               child = nodeService.createNode(current, ContentModel.ASSOC_CONTAINS, QName.createQName("testNodeAC"), ContentModel.TYPE_CONTENT);
//               ctx.childNode = child.getChildRef();
//               nodeService.setProperty(  ctx.childNode , ContentModel.PROP_TITLE, CONTENT_TITLE + "AC");   
//               nodeService.setProperty(  ctx.childNode , ContentModel.PROP_NAME, "DemoNodeAC");
//             
//                 {
//                     ContentWriter writer = contentService.getWriter(  ctx.childNode , ContentModel.PROP_CONTENT, true);
//                     writer.setLocale(CONTENT_LOCALE);
//                     writer.putContent(CONTENT_STRING);
//                     nodes.add(  ctx.childNode);
//                 }
//         
//                
//                /**
//                 * Now go ahead and create our first transfer target
//                 */
//                if(!transferService.targetExists(targetName))
//                {
//                    ctx.transferMe = createTransferTarget(targetName);
//                }
//                else
//                {
//                    ctx.transferMe = transferService.getTransferTarget(targetName);
//                }       
//                
//                return ctx;
//            } 
//        };
//        
//        final TestContext testContext = tran.doInTransaction(setupCB); 
//        
//        RetryingTransactionCallback<Void> transferCB = new RetryingTransactionCallback<Void>() {
//
//            @Override
//            public Void execute() throws Throwable
//            {
//                TransferDefinition definition = new TransferDefinition();
//                definition.setNodes(nodes);
//                transferService.transfer(targetName, definition);
//              
//               return null;
//            }
//        };
//        
//        tran.doInTransaction(transferCB); 
//        
//        RetryingTransactionCallback<Void> check1CB = new RetryingTransactionCallback<Void>() {
//
//            @Override
//            public Void execute() throws Throwable
//            {
//                NodeRef destChildNode = testNodeFactory.getMappedNodeRef(testContext.childNode);
//                assertTrue("dest node does not exist", nodeService.exists(destChildNode));
//                
//                /**
//                 * Step through source and dest trees on nodes comparing the path as we go.
//                 */
//                Path srcPath = nodeService.getPath(testContext.childNode);
//                Path destPath = nodeService.getPath(destChildNode);
//                
//                int srcSize = srcPath.size();
//                int destSize = destPath.size();
//                
//                Path dest = destPath.subPath(2, destSize-1);
//                Path src = srcPath.subPath(3, srcSize-1);
//                
////                System.out.println("src=" + src);
////                System.out.println("dest=" + dest);
//                assertEquals("paths are different", src.toString(), dest.toString());
//                             
//                return null;
//            }
//        };
//        
//        tran.doInTransaction(check1CB); 
//        
//        RetryingTransactionCallback<Void> updateCB = new RetryingTransactionCallback<Void>() {
//
//            @Override
//            public Void execute() throws Throwable
//            {
//              
//               return null;
//            }
//        };
//        
//        tran.doInTransaction(updateCB);
//        
//        tran.doInTransaction(transferCB); 
//        
//        RetryingTransactionCallback<Void> check2CB = new RetryingTransactionCallback<Void>() {
//
//            @Override
//            public Void execute() throws Throwable
//            {
//                assertTrue("dest node does not exist", nodeService.exists(testNodeFactory.getMappedNodeRef(testContext.childNode)));
//                
//               return null;
//            }
//        };
//        tran.doInTransaction(check2CB); 
//             
//    } // horrible paths

    /**
     * ALF-6174
     * Test transfer of peer associations
     * 
     * Step 1 : Create 2 nodes 
     * Add a peer assoc
     * Transfer
     * 
     * Step 2: Add another peer assoc
     * Transfer
     * 
     * Step 3: Remove a peer assoc
     * Transfer
     * 
     * Step 4: Remove a peer assoc
     * Transfer
     * 
     * @throws Exception
     */
    @Test
    public void testPeerAssocs() throws Exception
    {
        final RetryingTransactionHelper tran = transactionService.getRetryingTransactionHelper();
        final String CONTENT_TITLE = "ContentTitle";
        final Locale CONTENT_LOCALE = Locale.GERMAN; 
        final String CONTENT_ENCODING = "UTF-8";

        /**
         *  For unit test 
         *  - replace the HTTP transport with the in-process transport
         *  - replace the node factory with one that will map node refs, paths etc.
         *  
         *  Fake Repository Id
         */
        final TransferTransmitter transmitter = new UnitTestInProcessTransmitterImpl(receiver, contentService, transactionService);
        transferServiceImpl.setTransmitter(transmitter);
        final UnitTestTransferManifestNodeFactory testNodeFactory = new UnitTestTransferManifestNodeFactory(this.transferManifestNodeFactory); 
        transferServiceImpl.setTransferManifestNodeFactory(testNodeFactory); 
        final List<Pair<Path, Path>> pathMap = testNodeFactory.getPathMap();
        // Map company_home/guest_home to company_home so tranferred nodes and moved "up" one level.
        pathMap.add(new Pair<Path, Path>(PathHelper.stringToPath(GUEST_HOME_XPATH_QUERY), PathHelper.stringToPath(COMPANY_HOME_XPATH_QUERY)));

        final String targetName = "testPeerAssocs";

        class TestContext
        {
            TransferTarget transferMe;
            NodeRef folderNodeRef;
            NodeRef sourceNodeRef;
            NodeRef targetNodeRef;
            NodeRef destSourceNodeRef;
            NodeRef destTargetNodeRef;
        };

        RetryingTransactionCallback<TestContext> setupCB = new RetryingTransactionCallback<TestContext>()
        {
            @Override
            public TestContext execute() throws Throwable
            {
                TestContext testContext = new TestContext();

                NodeRef guestHome = repositoryHelper.getGuestHome();

                /**
                 * Create a test node that we will read and write
                 */  
                String name = GUID.generate();

                TransferDefinition def = new TransferDefinition();

                ChildAssociationRef child = nodeService.createNode(guestHome, ContentModel.ASSOC_CONTAINS, QName.createQName(name), ContentModel.TYPE_FOLDER);
                testContext.folderNodeRef = child.getChildRef();
                nodeService.setProperty(testContext.folderNodeRef, ContentModel.PROP_TITLE, CONTENT_TITLE);   
                nodeService.setProperty(testContext.folderNodeRef, ContentModel.PROP_NAME, name);

                // Side effect - initialisee nodeid mapping
                testNodeFactory.createTransferManifestNode(testContext.folderNodeRef, def, new TransferContext());

                child = nodeService.createNode(testContext.folderNodeRef, ContentModel.ASSOC_CONTAINS, QName.createQName("source"), ContentModel.TYPE_CONTENT);
                testContext.sourceNodeRef = child.getChildRef();
                nodeService.setProperty(testContext.sourceNodeRef, ContentModel.PROP_TITLE, CONTENT_TITLE);   
                nodeService.setProperty(testContext.sourceNodeRef, ContentModel.PROP_NAME, "source");

                // Side effect - initialise nodeid mapping
                testNodeFactory.createTransferManifestNode(testContext.sourceNodeRef, def, new TransferContext());

                child = nodeService.createNode(testContext.folderNodeRef, ContentModel.ASSOC_CONTAINS, QName.createQName("target"), ContentModel.TYPE_CONTENT);
                testContext.targetNodeRef = child.getChildRef();
                nodeService.setProperty(testContext.targetNodeRef, ContentModel.PROP_TITLE, CONTENT_TITLE);   
                nodeService.setProperty(testContext.targetNodeRef, ContentModel.PROP_NAME, "target");
                testNodeFactory.createTransferManifestNode(testContext.folderNodeRef, def, new TransferContext());
                nodeService.createAssociation(testContext.sourceNodeRef, testContext.targetNodeRef,  ContentModel.ASSOC_REFERENCES);

                // Side effect - initialise nodeid mapping
                testNodeFactory.createTransferManifestNode(testContext.targetNodeRef, def, new TransferContext());

                /**
                 * Make sure the transfer target exists and is enabled.
                 */
                if(!transferService.targetExists(targetName))
                {
                    testContext.transferMe = createTransferTarget(targetName);
                }
                else
                {
                    testContext.transferMe = transferService.getTransferTarget(targetName);
                }
                transferService.enableTransferTarget(targetName, true);
                return testContext;
            } 
        };

        final TestContext testContext = tran.doInTransaction(setupCB); 

        RetryingTransactionCallback<Void> addPeerAssocCB = new RetryingTransactionCallback<Void>() {

            public QName assocQName = ContentModel.ASSOC_ATTACHMENTS;

            @Override
            public Void execute() throws Throwable
            {
                nodeService.createAssociation(testContext.sourceNodeRef, testContext.targetNodeRef,  assocQName);

                return null;
            }
        };

        RetryingTransactionCallback<Void> removePeerAssocCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                List<AssociationRef> refs = nodeService.getTargetAssocs(testContext.sourceNodeRef, RegexQNamePattern.MATCH_ALL);
                if(refs.size() > 0)
                {
                    AssociationRef ref = refs.get(0);
                    nodeService.removeAssociation(ref.getSourceRef(), ref.getTargetRef(),  ref.getTypeQName());
                }
                return null;
            }
        };

        RetryingTransactionCallback<Void> transferCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                TransferDefinition definition = new TransferDefinition();
                Set<NodeRef>nodes = new HashSet<NodeRef>();
                nodes.add(testContext.sourceNodeRef);
                nodes.add(testContext.targetNodeRef);
                nodes.add(testContext.folderNodeRef);
                definition.setNodes(nodes);
                transferService.transfer(targetName, definition);
                return null;
            }
        };

        RetryingTransactionCallback<List<AssociationRef>> readAssocsCB = new RetryingTransactionCallback<List<AssociationRef>>() {

            @Override
            public List<AssociationRef>  execute() throws Throwable
            {
                List<AssociationRef> source = nodeService.getSourceAssocs(testContext.sourceNodeRef, RegexQNamePattern.MATCH_ALL);
                List<AssociationRef> target = nodeService.getTargetAssocs(testContext.sourceNodeRef, RegexQNamePattern.MATCH_ALL);

                NodeRef destNode = testNodeFactory.getMappedNodeRef(testContext.sourceNodeRef);
                List<AssociationRef> destSource = nodeService.getSourceAssocs(destNode, RegexQNamePattern.MATCH_ALL);
                List<AssociationRef> destTarget = nodeService.getTargetAssocs(destNode, RegexQNamePattern.MATCH_ALL);

                assertEquals("source peers different sizes",  destSource.size(), source.size() );
                assertEquals("target peers different sizes",  destTarget.size(), target.size() );

                if(destSource.size() == 1)
                {
                    assertEquals(destSource.get(0).getTypeQName(), source.get(0).getTypeQName());
                }

                if(destTarget.size() == 1)
                {
                    assertEquals(destTarget.get(0).getTypeQName(), target.get(0).getTypeQName());
                }

                return destTarget;
            }
        };

        /**
         * This is the test
         */

        tran.doInTransaction(transferCB);

        List<AssociationRef> assocs = tran.doInTransaction(readAssocsCB); 
        assertEquals("assocs not one", 1, assocs.size());

        tran.doInTransaction(addPeerAssocCB);

        tran.doInTransaction(transferCB); 

        assocs = tran.doInTransaction(readAssocsCB); 
        assertEquals("assocs not two", 2, assocs.size());

        tran.doInTransaction(removePeerAssocCB);

        tran.doInTransaction(transferCB); 

        tran.doInTransaction(removePeerAssocCB);

        tran.doInTransaction(transferCB); 

    } // testPeerAssocs
    
    
    /**
     * Test Existing nodes.  ALF-12262
     * 
     * Guest Home
     *     |
     *    GUID
     *     |
     *   A1  B1
     *   |   |
     *   A2  B2
     * 
     * @throws Exception
     */
    @Test
    public void testExistingNodes() throws Exception
    {
        final RetryingTransactionHelper tran = transactionService.getRetryingTransactionHelper();
        final String CONTENT_TITLE = "ContentTitle";
        final Locale CONTENT_LOCALE = Locale.GERMAN; 
        final String CONTENT_ENCODING = "UTF-8";

        /**
         *  For unit test 
         *  - replace the HTTP transport with the in-process transport
         *  - replace the node factory with one that will map node refs, paths etc.
         *  
         *  Fake Repository Id
         */
        final TransferTransmitter transmitter = new UnitTestInProcessTransmitterImpl(receiver, contentService, transactionService);
        transferServiceImpl.setTransmitter(transmitter);
        final UnitTestTransferManifestNodeFactory testNodeFactory = new UnitTestTransferManifestNodeFactory(this.transferManifestNodeFactory); 
        transferServiceImpl.setTransferManifestNodeFactory(testNodeFactory); 
        final List<Pair<Path, Path>> pathMap = testNodeFactory.getPathMap();
        
        // Map company_home/guest_home to company_home so tranferred nodes and moved "up" one level.
        pathMap.add(new Pair<Path, Path>(PathHelper.stringToPath(GUEST_HOME_XPATH_QUERY), PathHelper.stringToPath(COMPANY_HOME_XPATH_QUERY)));

        final String targetName = "testExistingNodes";

        class TestContext
        {
            TransferTarget transferMe;
            NodeRef folderNodeRef;
            NodeRef contentNodeRef;
            NodeRef rootNodeRef;
            NodeRef destFolderNodeRef;
            NodeRef destFileNodeRef;
        };

        RetryingTransactionCallback<TestContext> setupCB = new RetryingTransactionCallback<TestContext>()
        {
            @Override
            public TestContext execute() throws Throwable
            {
                TestContext testContext = new TestContext();

                NodeRef guestHome = repositoryHelper.getGuestHome();
                NodeRef companyHome = repositoryHelper.getCompanyHome();

                /**
                 * Create a test node that we will read and write
                 */  
                String name = GUID.generate();

                TransferDefinition def = new TransferDefinition();

                ChildAssociationRef child = nodeService.createNode(guestHome, ContentModel.ASSOC_CONTAINS, QName.createQName(name), ContentModel.TYPE_FOLDER);
                testContext.rootNodeRef = child.getChildRef();
                nodeService.setProperty(testContext.rootNodeRef, ContentModel.PROP_TITLE, name);   
                nodeService.setProperty(testContext.rootNodeRef, ContentModel.PROP_NAME, name);

                // Side effect - initialisee nodeid mapping
                testNodeFactory.createTransferManifestNode(testContext.rootNodeRef, def, new TransferContext());

                child = nodeService.createNode(testContext.rootNodeRef, ContentModel.ASSOC_CONTAINS, QName.createQName("A1"), ContentModel.TYPE_FOLDER);
                testContext.folderNodeRef = child.getChildRef();
                nodeService.setProperty(testContext.folderNodeRef, ContentModel.PROP_TITLE, "A1");   
                nodeService.setProperty(testContext.folderNodeRef, ContentModel.PROP_NAME, "A1");

                // Side effect - initialise nodeid mapping
                testNodeFactory.createTransferManifestNode(testContext.folderNodeRef, def, new TransferContext());

                child = nodeService.createNode(testContext.folderNodeRef, ContentModel.ASSOC_CONTAINS, QName.createQName("A2"), ContentModel.TYPE_CONTENT);
                testContext.contentNodeRef = child.getChildRef();
                nodeService.setProperty(testContext.contentNodeRef, ContentModel.PROP_TITLE, "A2");   
                nodeService.setProperty(testContext.contentNodeRef, ContentModel.PROP_NAME, "A2");
           
                // Side effect - initialise nodeid mapping
                testNodeFactory.createTransferManifestNode(testContext.contentNodeRef, def, new TransferContext());
                
      
                // Put nodes into destination
                child = nodeService.createNode(companyHome, ContentModel.ASSOC_CONTAINS, QName.createQName(name), ContentModel.TYPE_FOLDER);
                //testContext.rootNodeRef = child.getChildRef();
                nodeService.setProperty(testContext.rootNodeRef, ContentModel.PROP_TITLE, name);   
                nodeService.setProperty(testContext.rootNodeRef, ContentModel.PROP_NAME, name);                
                
                child = nodeService.createNode(child.getChildRef(), ContentModel.ASSOC_CONTAINS, QName.createQName("A1"), ContentModel.TYPE_FOLDER);
                testContext.destFolderNodeRef = child.getChildRef();
                nodeService.setProperty(testContext.destFolderNodeRef, ContentModel.PROP_TITLE, "A1");   
                nodeService.setProperty(testContext.destFolderNodeRef, ContentModel.PROP_NAME, "A1");

                child = nodeService.createNode(testContext.destFolderNodeRef, ContentModel.ASSOC_CONTAINS, QName.createQName("A2"), ContentModel.TYPE_CONTENT);
                testContext.destFileNodeRef = child.getChildRef();
                nodeService.setProperty(testContext.destFileNodeRef, ContentModel.PROP_TITLE, "A2");   
                nodeService.setProperty(testContext.destFileNodeRef, ContentModel.PROP_NAME, "A2");
                
                /**
                 * Make sure the transfer target exists and is enabled.
                 */
                if(!transferService.targetExists(targetName))
                {
                    testContext.transferMe = createTransferTarget(targetName);
                }
                else
                {
                    testContext.transferMe = transferService.getTransferTarget(targetName);
                }
                transferService.enableTransferTarget(targetName, true);
                return testContext;
            } 
        };

        final TestContext testContext = tran.doInTransaction(setupCB); 

        RetryingTransactionCallback<Void> transferCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                TransferDefinition definition = new TransferDefinition();
                Set<NodeRef>nodes = new HashSet<NodeRef>();
                nodes.add(testContext.rootNodeRef);
                nodes.add(testContext.folderNodeRef);
                nodes.add(testContext.contentNodeRef);
                definition.setNodes(nodes);
                transferService.transfer(targetName, definition);
                return null;
            }
        };
        
        RetryingTransactionCallback<Void> validateCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                assertTrue("folder has transferred aspect", !nodeService.hasAspect(testContext.destFolderNodeRef, TransferModel.ASPECT_TRANSFERRED));
                assertTrue("file has transferred aspctet", !nodeService.hasAspect(testContext.destFileNodeRef, TransferModel.ASPECT_TRANSFERRED));
                return null;
            }
        };

        /**
         * This is the test
         */

        tran.doInTransaction(transferCB);
        tran.doInTransaction(validateCB);

    } // testExistingNodes   
    
    /**
     * Test categories and tags (CRUD).
     * 
     * Step 1: Create a new node with a tag
     * transfer
     * 
     * Step 2: Add another tag
     * transfer
     *  
     * Step 3: Delete a tag
     * transfer
     * 
     * Step 4: Add a category
     * transfer
     * 
     * Step 5: Add another category this one deep
     * transfer
     * 
     * Step 6: Delete a category
     * transfer
     * 
     * This is a unit test so it does some shenanigans to send to the same instance of alfresco.
     */
    @Category(RedundantTests.class)
    @Test
    public void testCategoriesAndTags() throws Exception
    {
        final String CONTENT_TITLE = "ContentTitle";
        final String CONTENT_TITLE_UPDATED = "ContentTitleUpdated";
        final Locale CONTENT_LOCALE = Locale.GERMAN; 
        final String CONTENT_STRING = "Hello World";
        final String CONTENT_UPDATE_STRING = "Foo Bar";
        final String targetName = "testCategoriesAndTags";
        final String TAG_1_NAME = "tag1";
        final String TAG_2_NAME = "tag2";

        class TestContext
        {
            TransferTarget transferMe;
            NodeRef contentNodeRef;
            NodeRef destNodeRef;
        };

        /**
         * Unit test kludge to transfer from guest home to company home
         */
        final UnitTestTransferManifestNodeFactory testNodeFactory = unitTestKludgeToTransferGuestHomeToCompanyHome();
        
        DescriptorService mockedDescriptorService = getMockDescriptorService(REPO_ID_A);
        transferServiceImpl.setDescriptorService(mockedDescriptorService);
        
        RetryingTransactionCallback<TestContext> setupCB = new RetryingTransactionCallback<TestContext>()
        {
            @Override
            public TestContext execute() throws Throwable
            {
                TestContext ctx = new TestContext();
                
                NodeRef guestHome = repositoryHelper.getGuestHome();

                /**
                 * Create a test node that we will read and write
                 */
                String name = GUID.generate();
                ChildAssociationRef child = nodeService.createNode(guestHome, ContentModel.ASSOC_CONTAINS, QName.createQName(name), ContentModel.TYPE_CONTENT);
                ctx.contentNodeRef = child.getChildRef();
                nodeService.setProperty(ctx.contentNodeRef, ContentModel.PROP_TITLE, CONTENT_TITLE);   
                nodeService.setProperty(ctx.contentNodeRef, ContentModel.PROP_NAME, name);

                // Add the TAG to be transferred
                taggingService.addTag(ctx.contentNodeRef, TAG_1_NAME);
                
                if(!transferService.targetExists(targetName))
                {
                    ctx.transferMe = createTransferTarget(targetName);
                }
                else
                {
                    ctx.transferMe = transferService.getTransferTarget(targetName);
                }
                transferService.enableTransferTarget(targetName, true);

                return ctx;
            } 
        };
        final TestContext testContext = transactionService.getRetryingTransactionHelper().doInTransaction(setupCB);

        /**
         * Step 1: Transfer our which has a tag
         */ 
        logger.debug("First transfer - create new node with a tag");
        RetryingTransactionCallback<Void> transferCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                TransferDefinition definition = new TransferDefinition();
                Set<NodeRef>nodes = new HashSet<NodeRef>();
                nodes.add(testContext.contentNodeRef);
                definition.setNodes(nodes);
                transferService.transfer(targetName, definition);
                return null;
            }
        };
        transactionService.getRetryingTransactionHelper().doInTransaction(transferCB);
        
        RetryingTransactionCallback<Void> validateStep1CB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                // Now validate that the target node exists and has similar properties to the source
                testContext.destNodeRef = testNodeFactory.getMappedNodeRef( testContext.contentNodeRef);
                assertFalse("unit test stuffed up - comparing with self",  testContext.destNodeRef.equals( testContext.transferMe.getNodeRef()));
                assertTrue("dest node ref does not exist", nodeService.exists( testContext.destNodeRef));
                assertEquals("title is wrong", (String)nodeService.getProperty( testContext.destNodeRef, ContentModel.PROP_TITLE), CONTENT_TITLE); 
                assertEquals("type is wrong", nodeService.getType( testContext.contentNodeRef), nodeService.getType( testContext.destNodeRef));
                
                List<String> tags = taggingService.getTags(testContext.contentNodeRef);
                assertNotNull(tags);
                assertEquals(1, tags.size());
                    assertTrue(tags.contains(TAG_1_NAME));
                
                // Now add another tag for step number 2
                taggingService.addTag(testContext.contentNodeRef, TAG_2_NAME);
                return null;
           
            }
        };
        transactionService.getRetryingTransactionHelper().doInTransaction(validateStep1CB);
        
        /**
         * Step 2:
         * Transfer our node again - With another tag 
         */
        logger.debug("Second transfer - add a second tag");
        transactionService.getRetryingTransactionHelper().doInTransaction(transferCB);
     
        RetryingTransactionCallback<Void> validateStep2CB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {

                // Now validate that the target node exists and has similar properties to the source
                assertFalse("unit test stuffed up - comparing with self", testContext.destNodeRef.equals(testContext.transferMe.getNodeRef()));
                assertTrue("dest node ref does not exist", nodeService.exists(testContext.destNodeRef));
                
                List<String> tags = taggingService.getTags(testContext.contentNodeRef);
                
                assertNotNull(tags);
                assertTrue(tags.size() == 2);
                assertTrue(tags.contains(TAG_1_NAME));
                assertTrue(tags.contains(TAG_2_NAME));
              
                return null;
            }
        };

        transactionService.getRetryingTransactionHelper().doInTransaction(validateStep2CB);
        
        /**
         * Step 3 - delete a tag
         */
        
        RetryingTransactionCallback<Void> deleteTagCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                taggingService.removeTag(testContext.contentNodeRef, TAG_2_NAME);
                return null;
            }
        };

        transactionService.getRetryingTransactionHelper().doInTransaction(deleteTagCB);

       
        logger.debug("Transfer again - this is to delete a tag");
        transactionService.getRetryingTransactionHelper().doInTransaction(transferCB);
        
        // should probably be in contentModel
        final QName ASPECT_GENERAL_CLASSIFIABLE = ContentModel.ASPECT_GEN_CLASSIFIABLE;
        
        RetryingTransactionCallback<Void> validateStep3CB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                assertFalse("unit test stuffed up - comparing with self", testContext.destNodeRef.equals(testContext.transferMe.getNodeRef()));
                assertTrue("dest node ref does not exist", nodeService.exists(testContext.destNodeRef));

                List<String> tags = taggingService.getTags(testContext.destNodeRef);
                assertNotNull(tags);
                assertTrue(tags.size() == 1);
                assertTrue(tags.contains(TAG_1_NAME));
             
                return null;
            }
        };

        transactionService.getRetryingTransactionHelper().doInTransaction(validateStep3CB);
        
        /**
         * Step 4 - update to add a category that already exists
         */
        logger.debug("Step 4 - add a category");
        
        RetryingTransactionCallback<Void> step4WriteContentCB = new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
            	// 
                StoreRef workspaceSpacesStore = new StoreRef("workspace", "SpacesStore");
              	Collection<ChildAssociationRef> rootCategories = categoryService.getRootCategories(workspaceSpacesStore, ASPECT_GENERAL_CLASSIFIABLE);
              	
            	NodeRef languageCategory = null; 
              	for (ChildAssociationRef ref : rootCategories)
              	{
              		if(ref.getQName().getLocalName().equalsIgnoreCase("LANGUAGES"))
              		{
              			languageCategory = ref.getChildRef();
              		}
              	}
              	
              	assertNotNull("language category is null", languageCategory);
        
              	ChildAssociationRef categoryRef = categoryService.getCategory(languageCategory, ASPECT_GENERAL_CLASSIFIABLE, "English");
              	
  //            	Collection<ChildAssociationRef> allCategories = categoryService.getCategories(workspaceSpacesStore, ASPECT_GENERAL_CLASSIFIABLE, Depth.ANY);
              	
              	assertNotNull("ENGLISH CATEGORY REF is null", categoryRef);
              	
              	List<NodeRef> newCats = new ArrayList<NodeRef>();
              	newCats.add(categoryRef.getChildRef());
              	
              	nodeService.setProperty(testContext.contentNodeRef, ContentModel.PROP_CATEGORIES, (Serializable)newCats);
              	
                return null;
            }
        };
        
        RetryingTransactionCallback<Void> validateStep4CB = new RetryingTransactionCallback<Void>() {

            @Override
             public Void execute() throws Throwable
             {
                assertFalse("unit test stuffed up - comparing with self", testContext.destNodeRef.equals(testContext.transferMe.getNodeRef()));
                assertTrue("dest node ref does not exist", nodeService.exists(testContext.destNodeRef));

         	    assertTrue("destination node is missing aspect general classifiable", nodeService.hasAspect(testContext.destNodeRef, ContentModel.ASPECT_GEN_CLASSIFIABLE));
         	    Serializable categories = nodeService.getProperty(testContext.destNodeRef, ContentModel.PROP_CATEGORIES);
         	    assertNotNull("categories is missing on destination node", categories);
              
                return null;
             }
         };

        transactionService.getRetryingTransactionHelper().doInTransaction(step4WriteContentCB);

        transactionService.getRetryingTransactionHelper().doInTransaction(transferCB);

        transactionService.getRetryingTransactionHelper().doInTransaction(validateStep4CB);

     } // testCategoriesAndTags
    
    // Utility methods below.
    private TransferTarget createTransferTarget(String name)
    {   
        String title = "title";
        String description = "description";
        String endpointProtocol = "http";
        String endpointHost = "MARKR02";
        int endpointPort = 7080;
        String endpointPath = "/alfresco/service/api/transfer";
        String username = "admin";
        char[] password = "admin".toCharArray();
    
        /**
         * Now go ahead and create our first transfer target
         */
        TransferTarget target = transferService.createAndSaveTransferTarget(name, title, description, endpointProtocol, endpointHost, endpointPort, endpointPath, username, password);
        return target;
    }
        
    private DescriptorService getMockDescriptorService(String repositoryId)
    {
        DescriptorService descriptorService = mock(DescriptorService.class);
        Descriptor descriptor = mock(Descriptor.class);
        when(descriptor.getId()).thenReturn(repositoryId);
        when(descriptorService.getCurrentRepositoryDescriptor()).thenReturn(descriptor);
        when(descriptorService.getServerDescriptor()).thenReturn(serverDescriptor);
        
        return descriptorService;
    }
}