/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.content.packager;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import mockit.NonStrictExpectations;
import org.apache.log4j.Logger;
import org.dspace.AbstractUnitTest;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.ResourcePolicy;
import org.dspace.authorize.factory.AuthorizeServiceFactory;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.authorize.service.ResourcePolicyService;
import org.dspace.content.*;
import org.dspace.content.crosswalk.CrosswalkException;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.*;
import org.dspace.core.*;
import org.dspace.core.factory.CoreServiceFactory;
import org.dspace.core.service.PluginService;

import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;

import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.eperson.factory.EPersonServiceFactory;
import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.GroupService;
import org.dspace.handle.factory.HandleServiceFactory;
import org.dspace.handle.service.HandleService;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.dspace.workflow.WorkflowException;
import org.junit.*;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;

import org.junit.rules.TemporaryFolder;

/**
 * Basic integration testing for the AIP Backup and Restore feature
 * https://wiki.duraspace.org/display/DSDOC5x/AIP+Backup+and+Restore
 * 
 * @author Tim Donohue
 */
public class ITDSpaceAIP extends AbstractUnitTest
{
    /** log4j category */
    private static final Logger log = Logger.getLogger(ITDSpaceAIP.class);
    
    protected CommunityService communityService = ContentServiceFactory.getInstance().getCommunityService();
    protected CollectionService collectionService = ContentServiceFactory.getInstance().getCollectionService();
    protected ItemService itemService = ContentServiceFactory.getInstance().getItemService();
    protected BundleService bundleService = ContentServiceFactory.getInstance().getBundleService();
    protected BitstreamService bitstreamService = ContentServiceFactory.getInstance().getBitstreamService();
    protected WorkspaceItemService workspaceItemService = ContentServiceFactory.getInstance().getWorkspaceItemService();
    protected InstallItemService installItemService = ContentServiceFactory.getInstance().getInstallItemService();
    protected HandleService handleService = HandleServiceFactory.getInstance().getHandleService();
    protected PluginService pluginService = CoreServiceFactory.getInstance().getPluginService();
    protected ConfigurationService configService = DSpaceServicesFactory.getInstance().getConfigurationService();
    protected ResourcePolicyService resourcePolicyService = AuthorizeServiceFactory.getInstance().getResourcePolicyService();
    protected GroupService groupService = EPersonServiceFactory.getInstance().getGroupService();
    protected AuthorizeService authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService();

    /** InfoMap multiple value separator (see saveObjectInfo() and assertObject* methods) **/
    private static final String valueseparator = "::";
    
    /** Handles for Test objects initialized in setUpClass() and used in various tests below **/
    private static String topCommunityHandle = null;
    private static String testCollectionHandle = null;
    private static String testItemHandle = null;
    private static String testMappedItemHandle = null;
    private static String submitterEmail = "aip-test@dspace.org";
    private Context context;
    
    /** Create a global temporary upload folder which will be cleaned up automatically by JUnit.
        NOTE: As a ClassRule, this temp folder is shared by ALL tests below. **/
    @ClassRule
    public static final TemporaryFolder uploadTempFolder = new TemporaryFolder();

    /** Create another temporary folder for AIPs. As a Rule, this one is *recreated* for each
        test, in order to ensure each test is standalone with respect to AIPs. **/
    @Rule
    public final TemporaryFolder aipTempFolder = new TemporaryFolder();
    
    /**
     * This method will be run during class initialization. It will initialize
     * shared resources required for all the tests. It is only run once.
     *
     * Other methods can be annotated with @Before here or in subclasses
     * but no execution order is guaranteed
     */
    @BeforeClass
    public static void setUpClass()
    {
        try
        {
            Context context = new Context();
            // Create a dummy Community hierarchy to test with
            // Turn off authorization temporarily to create some test objects.
            context.turnOffAuthorisationSystem();

            CommunityService communityService = ContentServiceFactory.getInstance().getCommunityService();
            CollectionService collectionService = ContentServiceFactory.getInstance().getCollectionService();
            ItemService itemService = ContentServiceFactory.getInstance().getItemService();
            BitstreamService bitstreamService = ContentServiceFactory.getInstance().getBitstreamService();
            WorkspaceItemService workspaceItemService = ContentServiceFactory.getInstance().getWorkspaceItemService();
            InstallItemService installItemService = ContentServiceFactory.getInstance().getInstallItemService();

            log.info("setUpClass() - CREATE TEST HIERARCHY");
            // Create a hierachy of sub-Communities and Collections and Items, 
            // which looks like this:
            //  "Top Community"
            //      - "Child Community"
            //          - "Grandchild Community"
            //              - "GreatGrandchild Collection"
            //                  - "GreatGrandchild Collection Item #1"
            //                  - "GreatGrandchild Collection Item #2"
            //                  - "Mapped Item" (mapped collection)
            //          - "Grandchild Collection"
            //              - "Grandchild Collection Item #1"
            //              - "Mapped Item" (owning collection)
            //
            Community topCommunity = communityService.create(null, context);
            communityService.addMetadata(context, topCommunity, MetadataSchema.DC_SCHEMA, "title", null, null, "Top Community");
            communityService.update(context, topCommunity);
            topCommunityHandle = topCommunity.getHandle();
            
            Community child = communityService.createSubcommunity(context, topCommunity);
            communityService.addMetadata(context, child, MetadataSchema.DC_SCHEMA, "title", null, null, "Child Community");
            communityService.update(context, child);
            
            Community grandchild = communityService.createSubcommunity(context, child);
            communityService.addMetadata(context, grandchild, MetadataSchema.DC_SCHEMA, "title", null, null, "Grandchild Community");
            communityService.update(context, grandchild);

            // Create our primary Test Collection
            Collection grandchildCol = collectionService.create(context, child);
            collectionService.addMetadata(context, grandchildCol, "dc", "title", null, null, "Grandchild Collection");
            collectionService.update(context, grandchildCol);
            testCollectionHandle = grandchildCol.getHandle();

            // Create an additional Test Collection
            Collection greatgrandchildCol = collectionService.create(context, grandchild);
            collectionService.addMetadata(context, greatgrandchildCol, "dc", "title", null, null, "GreatGrandchild Collection");
            collectionService.update(context, greatgrandchildCol);

            EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
            EPerson submitter = ePersonService.create(context);
            submitter.setEmail(submitterEmail);
            ePersonService.update(context, submitter);
            context.setCurrentUser(submitter);

            //Make our test ePerson an admin so he can perform deletes and restores
            GroupService groupService = EPersonServiceFactory.getInstance().getGroupService();
            Group adminGroup = groupService.findByName(context, Group.ADMIN);
            groupService.addMember(context, adminGroup, submitter);

            // Create our primary Test Item
            WorkspaceItem wsItem = workspaceItemService.create(context, grandchildCol, false);
            Item item = installItemService.installItem(context, wsItem);
            itemService.addMetadata(context, item, "dc", "title", null, null, "Grandchild Collection Item #1");
            // For our primary test item, create a Bitstream in the ORIGINAL bundle
            File f = new File(testProps.get("test.bitstream").toString());
            Bitstream b = itemService.createSingleBitstream(context, new FileInputStream(f), item);
            b.setName(context, "Test Bitstream");
            bitstreamService.update(context, b);
            itemService.update(context, item);
            testItemHandle = item.getHandle();

            // Create a Mapped Test Item (mapped to multiple collections
            WorkspaceItem wsItem2 = workspaceItemService.create(context, grandchildCol, false);
            Item item2 = installItemService.installItem(context, wsItem2);
            itemService.addMetadata(context, item2, "dc", "title", null, null, "Mapped Item");
            itemService.update(context, item2);
            collectionService.addItem(context, greatgrandchildCol, item2);
            testMappedItemHandle = item2.getHandle();

            WorkspaceItem wsItem3 = workspaceItemService.create(context, greatgrandchildCol, false);
            Item item3 = installItemService.installItem(context, wsItem3);
            itemService.addMetadata(context, item3, "dc", "title", null, null, "GreatGrandchild Collection Item #1");
            itemService.update(context, item3);

            WorkspaceItem wsItem4 = workspaceItemService.create(context, greatgrandchildCol, false);
            Item item4 = installItemService.installItem(context, wsItem4);
            itemService.addMetadata(context, item4, "dc", "title", null, null, "GreatGrandchild Collection Item #2");
            itemService.update(context, item4);

            // Commit these changes to our DB
            context.restoreAuthSystemState();
            context.complete();
        }
        catch (AuthorizeException ex)
        {
            log.error("Authorization Error in setUpClass()", ex);
            fail("Authorization Error in setUpClass(): " + ex.getMessage());
        }
        catch (IOException ex)
        {
            log.error("IO Error in setUpClass()", ex);
            fail("IO Error in setUpClass(): " + ex.getMessage());
        }
        catch (SQLException ex)
        {
            log.error("SQL Error in setUpClass()", ex);
            fail("SQL Error in setUpClass(): " + ex.getMessage());
        }
    }
    
    /**
     * This method will be run once at the very end
     */
    @AfterClass
    public static void tearDownClass()
    {
        try
        {
            Context context = new Context();
            CommunityService communityService = ContentServiceFactory.getInstance().getCommunityService();
            HandleService handleService = HandleServiceFactory.getInstance().getHandleService();
            Community topCommunity = (Community) handleService.resolveToObject(context, topCommunityHandle);

            // Delete top level test community and test hierarchy under it
            if(topCommunity!=null)
            {
                log.info("tearDownClass() - DESTROY TEST HIERARCHY");
                context.turnOffAuthorisationSystem();
                communityService.delete(context, topCommunity);
                context.restoreAuthSystemState();
                context.commit();
            }

            // Delete the Eperson created to submit test items
            EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
            EPerson eperson = ePersonService.findByEmail(context, submitterEmail);
            if(eperson!=null)
            {
                log.info("tearDownClass() - DESTROY TEST EPERSON");
                context.turnOffAuthorisationSystem();
                ePersonService.delete(context, eperson);
                context.restoreAuthSystemState();
                context.commit();
            }

            if(context.isValid())
                context.abort();
        }
        catch (Exception ex)
        {
            log.error("Error in tearDownClass()", ex);
        }
    }
    
    /**
     * Create an initial set of AIPs for the test content generated in setUpClass() above.
     */
    @Before
    @Override
    public void init()
    {
        // call init() from AbstractUnitTest to initialize testing framework
        super.init();

        // Override default value of configured temp directory to point at our
        // JUnit TemporaryFolder. This ensures Crosswalk classes like RoleCrosswalk
        // store their temp files in a place where JUnit can clean them up automatically.
        new NonStrictExpectations(configService.getClass()) {{
            configService.getProperty("upload.temp.dir"); result = uploadTempFolder.getRoot().getAbsolutePath();
        }};
        
        try
        {
            context = new Context();
            context.setCurrentUser(EPersonServiceFactory.getInstance().getEPersonService().findByEmail(context, submitterEmail));
        }
        catch (SQLException ex)
        {
            log.error("SQL Error in init()", ex);
            fail("SQL Error in init(): " + ex.getMessage());
        }
    }

    @After
    @Override
    public void destroy() {
        context.abort();
        super.destroy();
    }

    /**
     * Test restoration from AIP of entire Community Hierarchy
     */
    @Test
    public void testRestoreCommunityHierarchy() throws Exception
    {
        new NonStrictExpectations(authorizeService.getClass())
        {{
            // Allow Full Admin permissions. Since we are working with an object
            // hierarchy you really need full admin rights
            authorizeService.isAdmin((Context) any); result = true;
        }};

        log.info("testRestoreCommunityHierarchy() - BEGIN");

        // Locate the top level community (from our test data)
        Community topCommunity = (Community) handleService.resolveToObject(context, topCommunityHandle);

        // Get parent object, so that we can restore to same parent later
        DSpaceObject parent = communityService.getParentObject(context, topCommunity);

        // Save basic info about top community (and children) to an infoMap
        HashMap<String,String> infoMap = new HashMap<String,String>();
        saveObjectInfo(topCommunity, infoMap);

        // Export community & child AIPs
        log.info("testRestoreCommunityHierarchy() - CREATE AIPs");
        File aipFile = createAIP(topCommunity, null, true);

        // Delete everything from parent community on down
        log.info("testRestoreCommunityHierarchy() - DELETE Community Hierarchy");
        communityService.delete(context, topCommunity);

        // Assert all objects in infoMap no longer exist in DSpace
        assertObjectsNotExist(infoMap);

        // Restore this Community (recursively) from AIPs
        log.info("testRestoreCommunityHierarchy() - RESTORE Community Hierarchy");
        // Ensure "skipIfParentMissing" flag is set to true. 
        // As noted in the documentation, this is often needed for larger, hierarchical
        // restores when you have Mapped Items (which we do in our test data)
        PackageParameters pkgParams = new PackageParameters();
        pkgParams.addProperty("skipIfParentMissing", "true");
        restoreFromAIP(parent, aipFile, pkgParams, true);

        // Assert all objects in infoMap now exist again!
        assertObjectsExist(infoMap);
        
        // SPECIAL CASE: Test Item Mapping restoration was successful
        // In our community, we have one Item which should be in two Collections
        Item mappedItem = (Item) handleService.resolveToObject(context, testMappedItemHandle);
        assertEquals("testRestoreCommunityHierarchy() - Mapped Item's Collection mappings restored", 2, mappedItem.getCollections().size());
        
        log.info("testRestoreCommunityHierarchy() - END");
    }
    
    /**
     * Test restoration from AIP of an access restricted Community
     */
    @Test
    public void testRestoreRestrictedCommunity() throws Exception
    {
        new NonStrictExpectations(authorizeService.getClass())
        {{
            // Allow Full Admin permissions. Since we are working with an object
            // hierarchy  (Items/Bundles/Bitstreams) you need full admin rights
            authorizeService.isAdmin((Context) any); result = true;
        }};

        log.info("testRestoreRestrictedCommunity() - BEGIN");

        // Locate the top-level Community (as a parent)
        Community parent = (Community) handleService.resolveToObject(context, topCommunityHandle);

        // Create a brand new (empty) Community to test with
        Community community = communityService.createSubcommunity(context, parent);
        communityService.addMetadata(context, community, "dc", "title", null, null, "Restricted Community");
        communityService.update(context, community);
        String communityHandle = community.getHandle();

        // Create a new Group to access restrict to
        Group group = groupService.create(context);
        groupService.setName(group, "Special Users");
        groupService.update(context, group);

        // Create a custom resource policy for this community
        List<ResourcePolicy> policies = new ArrayList<>();
        ResourcePolicy policy = resourcePolicyService.create(context);
        policy.setRpName("Special Read Only");
        policy.setGroup(group);
        policy.setAction(Constants.READ);
        policies.add(policy);

        // Replace default community policies with this new one
        authorizeService.removeAllPolicies(context, community);
        authorizeService.addPolicies(context, policies, community);

        // Export collection AIP
        log.info("testRestoreRestrictedCommunity() - CREATE Community AIP");
        File aipFile = createAIP(community, null, false);

        // Now, delete that Collection
        log.info("testRestoreRestrictedCommunity() - DELETE Community");
        communityService.removeSubcommunity(context, parent, community);

        // Assert the deleted collection no longer exists
        DSpaceObject obj = handleService.resolveToObject(context, communityHandle);
        assertThat("testRestoreRestrictedCommunity() Community " + communityHandle + " doesn't exist", obj, nullValue());

        // Restore Collection from AIP (non-recursive)
        log.info("testRestoreRestrictedCommunity() - RESTORE Community");
        restoreFromAIP(parent, aipFile, null, false);

        // Assert the deleted Collection is RESTORED
        DSpaceObject objRestored = handleService.resolveToObject(context, communityHandle);
        assertThat("testRestoreRestrictedCommunity() Community " + communityHandle + " exists", objRestored, notNullValue());

        // Assert the number of restored policies is equal
        List<ResourcePolicy> policiesRestored = authorizeService.getPolicies(context, objRestored);
        assertEquals("testRestoreRestrictedCommunity() restored policy count equal", policies.size(), policiesRestored.size());

        // Assert the restored policy has same name, group and permission settings
        ResourcePolicy restoredPolicy = policiesRestored.get(0);
        assertEquals("testRestoreRestrictedCommunity() restored policy group successfully", policy.getGroup().getName(), restoredPolicy.getGroup().getName());
        assertEquals("testRestoreRestrictedCommunity() restored policy action successfully", policy.getAction(), restoredPolicy.getAction());
        assertEquals("testRestoreRestrictedCommunity() restored policy name successfully", policy.getRpName(), restoredPolicy.getRpName());

        log.info("testRestoreRestrictedCommunity() - END");
    }
    
    /**
     * Test replacement from AIP of entire Community Hierarchy
     */
    @Test
    public void testReplaceCommunityHierarchy() throws Exception
    {
        new NonStrictExpectations(authorizeService.getClass())
        {{
            // Allow Full Admin permissions. Since we are working with an object
            // hierarchy you really need full admin rights
            authorizeService.isAdmin((Context) any); result = true;
        }};

        log.info("testReplaceCommunityHierarchy() - BEGIN");

        // Locate the top level community (from our test data)
        Community topCommunity = (Community) handleService.resolveToObject(context, topCommunityHandle);

        // Get the count of collections under our Community or any Sub-Communities
        int numberOfCollections = communityService.getAllCollections(context, topCommunity).size();
        
        // Export community & child AIPs
        log.info("testReplaceCommunityHierarchy() - CREATE AIPs");
        File aipFile = createAIP(topCommunity, null, true);
        
        // Get some basic info about Collection to be deleted
        // In this scenario, we'll delete the test "Grandchild Collection" 
        // (which is initialized as being under the Top Community)
        String deletedCollectionHandle = testCollectionHandle;
        Collection collectionToDelete = (Collection) handleService.resolveToObject(context, deletedCollectionHandle);
        Community parent = (Community) collectionService.getParentObject(context, collectionToDelete);
         
        // How many items are in this Collection we are about to delete?
        int numberOfItems = itemService.countItems(context, collectionToDelete);
        // Get an Item that should be deleted when we delete this Collection
        // (NOTE: This item is initialized to be a member of the deleted Collection)
        String deletedItemHandle = testItemHandle;
        
        // Now, delete that one collection
        log.info("testReplaceCommunityHierarchy() - DELETE Collection");
        communityService.removeCollection(context, parent, collectionToDelete);

        // Assert the deleted collection no longer exists
        DSpaceObject obj = handleService.resolveToObject(context, deletedCollectionHandle);
        assertThat("testReplaceCommunityHierarchy() collection " + deletedCollectionHandle + " doesn't exist", obj, nullValue());
        
        // Assert the child item no longer exists
        DSpaceObject obj2 = handleService.resolveToObject(context, deletedItemHandle);
        assertThat("testReplaceCommunityHierarchy() item " + deletedItemHandle + " doesn't exist", obj2, nullValue());

        // Replace Community (and all child objects, recursively) from AIPs
        log.info("testReplaceCommunityHierarchy() - REPLACE Community Hierarchy");
        // Ensure "skipIfParentMissing" flag is set to true. 
        // As noted in the documentation, this is often needed for larger, hierarchical
        // replacements when you have Mapped Items (which we do in our test data)
        PackageParameters pkgParams = new PackageParameters();
        pkgParams.addProperty("skipIfParentMissing", "true");
        replaceFromAIP(topCommunity, aipFile, pkgParams, true);

        // Assert the deleted collection is RESTORED
        DSpaceObject objRestored = handleService.resolveToObject(context, deletedCollectionHandle);
        assertThat("testReplaceCommunityHierarchy() collection " + deletedCollectionHandle + " exists", objRestored, notNullValue());
        
        // Assert the deleted item is also RESTORED
        DSpaceObject obj2Restored = handleService.resolveToObject(context, deletedItemHandle);
        assertThat("testReplaceCommunityHierarchy() item " + deletedItemHandle + " exists", obj2Restored, notNullValue());

        // Assert the Collection count and Item count are same as before
        assertEquals("testReplaceCommunityHierarchy() collection count", numberOfCollections, communityService.getAllCollections(context, topCommunity).size());
        assertEquals("testReplaceCommunityHierarchy() item count", numberOfItems, itemService.countItems(context, ((Collection) objRestored)));
    
        log.info("testReplaceCommunityHierarchy() - END");
    }
    
    /**
     * Test replacement from AIP of JUST a Community object
     */
    @Test
    public void testReplaceCommunityOnly() throws Exception
    {
        new NonStrictExpectations(authorizeService.getClass())
        {{
            // Allow Community WRITE perms
            authorizeService.authorizeAction((Context) any, (Community) any,
                    Constants.WRITE,true); result = null;
        }};
        
        log.info("testReplaceCommunityOnly() - BEGIN");

        // Locate the top level community (from our test data)
        Community topCommunity = (Community) handleService.resolveToObject(context, topCommunityHandle);

        // Get its current name / title
        String oldName = topCommunity.getName();
        
        // Export only community AIP
        log.info("testReplaceCommunityOnly() - CREATE Community AIP");
        File aipFile = createAIP(topCommunity, null, false);
        
        // Change the Community name
        String newName = "This is NOT my Community name!";
        communityService.clearMetadata(context, topCommunity, MetadataSchema.DC_SCHEMA, "title", null, Item.ANY);
        communityService.addMetadata(context, topCommunity, MetadataSchema.DC_SCHEMA, "title", null, null, newName);

        // Ensure name is changed
        assertEquals("testReplaceCommunityOnly() new name", topCommunity.getName(), newName);
        
        // Now, replace our Community from AIP (non-recursive)
        replaceFromAIP(topCommunity, aipFile, null, false);

        // Check if name reverted to previous value
        assertEquals("testReplaceCommunityOnly() old name", topCommunity.getName(), oldName);
    }
    
    /**
     * Test restoration from AIP of entire Collection Hierarchy
     */
    @Test
    public void testRestoreCollectionHierarchy() throws Exception
    {
        new NonStrictExpectations(authorizeService.getClass())
        {{
            // Allow Full Admin permissions. Since we are working with an object
            // hierarchy you really need full admin rights
            authorizeService.isAdmin((Context) any); result = true;
        }};

        log.info("testRestoreCollectionHierarchy() - BEGIN");

        // Locate the collection (from our test data)
        Collection testCollection = (Collection) handleService.resolveToObject(context, testCollectionHandle);
        
        // Get parent object, so that we can restore to same parent later
        Community parent = (Community) collectionService.getParentObject(context, testCollection);

        // Save basic info about collection (and children) to an infoMap
        HashMap<String,String> infoMap = new HashMap<String,String>();
        saveObjectInfo(testCollection, infoMap);

        // Export collection & child AIPs
        log.info("testRestoreCollectionHierarchy() - CREATE AIPs");
        File aipFile = createAIP(testCollection, null, true);

        // Delete everything from collection on down
        log.info("testRestoreCollectionHierarchy() - DELETE Collection Hierarchy");
        communityService.removeCollection(context, parent, testCollection);

        // Assert all objects in infoMap no longer exist in DSpace
        assertObjectsNotExist(infoMap);

        // Restore this Collection (recursively) from AIPs
        log.info("testRestoreCollectionHierarchy() - RESTORE Collection Hierarchy");
        restoreFromAIP(parent, aipFile, null, true);

        // Assert all objects in infoMap now exist again!
        assertObjectsExist(infoMap);
        
        log.info("testRestoreCollectionHierarchy() - END");
    }
    
    /**
     * Test restoration from AIP of an access restricted Collection
     */
    @Test
    public void testRestoreRestrictedCollection() throws Exception
    {
        new NonStrictExpectations(authorizeService.getClass())
        {{
            // Allow Full Admin permissions. Since we are working with an object
            // hierarchy  (Items/Bundles/Bitstreams) you need full admin rights
            authorizeService.isAdmin((Context) any); result = true;
        }};

        log.info("testRestoreRestrictedCollection() - BEGIN");

        // Locate the top-level Community (as a parent)
        Community parent = (Community) handleService.resolveToObject(context, topCommunityHandle);

        // Create a brand new (empty) Collection to test with
        Collection collection = collectionService.create(context, parent);
        collectionService.addMetadata(context, collection, "dc", "title", null, null, "Restricted Collection");
        collectionService.update(context, collection);
        String collectionHandle = collection.getHandle();

        // Create a new Group to access restrict to
        Group group = groupService.create(context);
        groupService.setName(group, "Special Users");
        groupService.update(context, group);

        // Create a custom resource policy for this Collection
        List<ResourcePolicy> policies = new ArrayList<>();
        ResourcePolicy policy = resourcePolicyService.create(context);
        policy.setRpName("Special Read Only");
        policy.setGroup(group);
        policy.setAction(Constants.READ);
        policies.add(policy);

        // Replace default Collection policies with this new one
        authorizeService.removeAllPolicies(context, collection);
        authorizeService.addPolicies(context, policies, collection);

        // Export collection AIP
        log.info("testRestoreRestrictedCollection() - CREATE Collection AIP");
        File aipFile = createAIP(collection, null, false);

        // Now, delete that Collection
        log.info("testRestoreRestrictedCollection() - DELETE Collection");
        communityService.removeCollection(context, parent, collection);

        // Assert the deleted collection no longer exists
        DSpaceObject obj = handleService.resolveToObject(context, collectionHandle);
        assertThat("testRestoreRestrictedCollection() Collection " + collectionHandle + " doesn't exist", obj, nullValue());

        // Restore Collection from AIP (non-recursive)
        log.info("testRestoreRestrictedCollection() - RESTORE Collection");
        restoreFromAIP(parent, aipFile, null, false);

        // Assert the deleted Collection is RESTORED
        DSpaceObject objRestored = handleService.resolveToObject(context, collectionHandle);
        assertThat("testRestoreRestrictedCollection() Collection " + collectionHandle + " exists", objRestored, notNullValue());

        // Assert the number of restored policies is equal
        List<ResourcePolicy> policiesRestored = authorizeService.getPolicies(context, objRestored);
        assertEquals("testRestoreRestrictedCollection() restored policy count equal", policies.size(), policiesRestored.size());

        // Assert the restored policy has same name, group and permission settings
        ResourcePolicy restoredPolicy = policiesRestored.get(0);
        assertEquals("testRestoreRestrictedCollection() restored policy group successfully", policy.getGroup().getName(), restoredPolicy.getGroup().getName());
        assertEquals("testRestoreRestrictedCollection() restored policy action successfully", policy.getAction(), restoredPolicy.getAction());
        assertEquals("testRestoreRestrictedCollection() restored policy name successfully", policy.getRpName(), restoredPolicy.getRpName());

        log.info("testRestoreRestrictedCollection() - END");
    }
    
    /**
     * Test replacement from AIP of entire Collection (with Items)
     */
    @Test
    public void testReplaceCollectionHierarchy() throws Exception
    {
        new NonStrictExpectations(authorizeService.getClass())
        {{
            // Allow Full Admin permissions. Since we are working with an object
            // hierarchy you really need full admin rights
            authorizeService.isAdmin((Context) any); result = true;
        }};

        log.info("testReplaceCollectionHierarchy() - BEGIN");

        // Locate the collection (from our test data)
        Collection testCollection = (Collection) handleService.resolveToObject(context, testCollectionHandle);
        
        // How many items are in this Collection?
        int numberOfItems = itemService.countItems(context, testCollection);
        
        // Export collection & child AIPs
        log.info("testReplaceCollectionHierarchy() - CREATE AIPs");
        File aipFile = createAIP(testCollection, null, true);
        
        // Get some basic info about Item to be deleted
        // In this scenario, we'll delete the test "Grandchild Collection Item #1" 
        // (which is initialized as being an Item within this Collection)
        String deletedItemHandle = testItemHandle;
        Item itemToDelete = (Item) handleService.resolveToObject(context, deletedItemHandle);
        Collection parent = (Collection) itemService.getParentObject(context, itemToDelete);
         
        // Now, delete that one item
        log.info("testReplaceCollectionHierarchy() - DELETE Item");
        collectionService.removeItem(context, parent, itemToDelete);

        // Assert the deleted item no longer exists
        DSpaceObject obj = handleService.resolveToObject(context, deletedItemHandle);
        assertThat("testReplaceCollectionHierarchy() item " + deletedItemHandle + " doesn't exist", obj, nullValue());
        
        // Assert the item count is one less
        assertEquals("testReplaceCollectionHierarchy() updated item count for collection " + testCollectionHandle, numberOfItems - 1, itemService.countItems(context, testCollection));

        // Replace Collection (and all child objects, recursively) from AIPs
        log.info("testReplaceCollectionHierarchy() - REPLACE Collection Hierarchy");
        replaceFromAIP(testCollection, aipFile, null, true);

        // Assert the deleted item is RESTORED
        DSpaceObject objRestored = handleService.resolveToObject(context, deletedItemHandle);
        assertThat("testReplaceCollectionHierarchy() item " + deletedItemHandle + " exists", objRestored, notNullValue());
        
        // Assert the Item count is same as before
        assertEquals("testReplaceCollectionHierarchy() restored item count for collection " + testCollectionHandle, numberOfItems, itemService.countItems(context, testCollection));

        log.info("testReplaceCollectionHierarchy() - END");
    }
    
    
    /**
     * Test replacement from AIP of JUST a Collection object
     */
    @Test
    public void testReplaceCollectionOnly() throws Exception
    {
        new NonStrictExpectations(authorizeService.getClass())
        {{
            // Allow Collection WRITE perms
            authorizeService.authorizeAction((Context) any, (Collection) any,
                    Constants.WRITE,true); result = null;
        }};
        
        log.info("testReplaceCollectionOnly() - BEGIN");

        // Locate the collection (from our test data)
        Collection testCollection = (Collection) handleService.resolveToObject(context, testCollectionHandle);
        
        // Get its current name / title
        String oldName = testCollection.getName();
        
        // Export only collection AIP
        log.info("testReplaceCollectionOnly() - CREATE Collection AIP");
        File aipFile = createAIP(testCollection, null, false);
        
        // Change the Collection name
        String newName = "This is NOT my Collection name!";
        collectionService.clearMetadata(context, testCollection, MetadataSchema.DC_SCHEMA, "title", null, Item.ANY);
        collectionService.addMetadata(context, testCollection, MetadataSchema.DC_SCHEMA, "title", null, null, newName);

        // Ensure name is changed
        assertEquals("testReplaceCollectionOnly() new name", testCollection.getName(), newName);
        
        // Now, replace our Collection from AIP (non-recursive)
        replaceFromAIP(testCollection, aipFile, null, false);

        // Check if name reverted to previous value
        assertEquals("testReplaceCollectionOnly() old name", testCollection.getName(), oldName);
    }
    
    
    /**
     * Test restoration from AIP of an Item
     */
    @Test
    public void testRestoreItem() throws Exception
    {
        new NonStrictExpectations(authorizeService.getClass())
        {{
            // Allow Full Admin permissions. Since we are working with an object
            // hierarchy  (Items/Bundles/Bitstreams) you need full admin rights
            authorizeService.isAdmin((Context) any); result = true;
        }};

        log.info("testRestoreItem() - BEGIN");

        // Locate the item (from our test data)
        Item testItem = (Item) handleService.resolveToObject(context, testItemHandle);
        
        // Get information about the Item's Bitstreams
        // (There should be one bitstream initialized above)
        int bitstreamCount = 0;
        String bitstreamName = null;
        String bitstreamCheckSum = null;
        List<Bundle> bundles = itemService.getBundles(testItem, Constants.CONTENT_BUNDLE_NAME);
        if(bundles.size()>0)
        {
            List<Bitstream> bitstreams = bundles.get(0).getBitstreams();
            bitstreamCount = bitstreams.size();
            if(bitstreamCount>0)
            {
                bitstreamName = bitstreams.get(0).getName();
                bitstreamCheckSum = bitstreams.get(0).getChecksum();
            }
        }

        // We need a test bitstream to work with!
        if(bitstreamCount<=0)
            fail("No test bitstream found for Item in testRestoreItem()!");

        // Export item AIP
        log.info("testRestoreItem() - CREATE Item AIP");
        File aipFile = createAIP(testItem, null, false);
        
        // Get parent, so we can restore under the same parent
        Collection parent = (Collection) itemService.getParentObject(context, testItem);
        
        // Now, delete that item
        log.info("testRestoreItem() - DELETE Item");
        collectionService.removeItem(context, parent, testItem);

        // Assert the deleted item no longer exists
        DSpaceObject obj = handleService.resolveToObject(context, testItemHandle);
        assertThat("testRestoreItem() item " + testItemHandle + " doesn't exist", obj, nullValue());
        
        // Restore Item from AIP (non-recursive)
        log.info("testRestoreItem() - RESTORE Item");
        restoreFromAIP(parent, aipFile, null, false);

        // Assert the deleted item is RESTORED
        DSpaceObject objRestored = handleService.resolveToObject(context, testItemHandle);
        assertThat("testRestoreItem() item " + testItemHandle + " exists", objRestored, notNullValue());
        
        // Assert Bitstream exists again & is associated with restored item
        List<Bundle> restoredBund = itemService.getBundles(((Item) objRestored), Constants.CONTENT_BUNDLE_NAME);
        Bitstream restoredBitstream = bundleService.getBitstreamByName(restoredBund.get(0), bitstreamName);
        assertThat("testRestoreItem() bitstream exists", restoredBitstream, notNullValue());
        assertEquals("testRestoreItem() bitstream checksum", restoredBitstream.getChecksum(), bitstreamCheckSum);

        log.info("testRestoreItem() - END");
    }
    
    /**
     * Test restoration from AIP of an access restricted Item
     */
    @Test
    public void testRestoreRestrictedItem() throws Exception
    {
        new NonStrictExpectations(authorizeService.getClass())
        {{
            // Allow Full Admin permissions. Since we are working with an object
            // hierarchy  (Items/Bundles/Bitstreams) you need full admin rights
            authorizeService.isAdmin((Context) any); result = true;
        }};

        log.info("testRestoreRestrictedItem() - BEGIN");

        // Locate the test Collection (as a parent)
        Collection parent = (Collection) handleService.resolveToObject(context, testCollectionHandle);

        // Create a brand new Item to test with (since we will be changing policies)
        WorkspaceItem wsItem = workspaceItemService.create(context, parent, false);
        Item item = installItemService.installItem(context, wsItem);
        itemService.addMetadata(context, item, "dc", "title", null, null, "Test Restricted Item");
        // Create a test Bitstream in the ORIGINAL bundle
        File f = new File(testProps.get("test.bitstream").toString());
        Bitstream b = itemService.createSingleBitstream(context, new FileInputStream(f), item);
        b.setName(context, "Test Bitstream");
        bitstreamService.update(context, b);
        itemService.update(context, item);

        // Create a custom resource policy for this Item
        List<ResourcePolicy> policies = new ArrayList<>();
        ResourcePolicy admin_policy = resourcePolicyService.create(context);
        admin_policy.setRpName("Admin Read-Only");
        Group adminGroup = groupService.findByName(context, Group.ADMIN);
        admin_policy.setGroup(adminGroup);
        admin_policy.setAction(Constants.READ);
        policies.add(admin_policy);
        itemService.replaceAllItemPolicies(context, item, policies);

        // Export item AIP
        log.info("testRestoreRestrictedItem() - CREATE Item AIP");
        File aipFile = createAIP(item, null, false);

        // Get item handle, so we can check that it is later restored properly
        String itemHandle = item.getHandle();

        // Now, delete that item
        log.info("testRestoreRestrictedItem() - DELETE Item");
        collectionService.removeItem(context, parent, item);

        // Assert the deleted item no longer exists
        DSpaceObject obj = handleService.resolveToObject(context, itemHandle);
        assertThat("testRestoreRestrictedItem() item " + itemHandle + " doesn't exist", obj, nullValue());

        // Restore Item from AIP (non-recursive)
        log.info("testRestoreRestrictedItem() - RESTORE Item");
        restoreFromAIP(parent, aipFile, null, false);

        // Assert the deleted item is RESTORED
        DSpaceObject objRestored = handleService.resolveToObject(context, itemHandle);
        assertThat("testRestoreRestrictedItem() item " + itemHandle + " exists", objRestored, notNullValue());

        // Assert the number of restored policies is equal
        List<ResourcePolicy> policiesRestored = authorizeService.getPolicies(context, objRestored);
        assertEquals("testRestoreRestrictedItem() restored policy count equal", policies.size(), policiesRestored.size());

        // Assert the restored policy has same name, group and permission settings
        ResourcePolicy restoredPolicy = policiesRestored.get(0);
        assertEquals("testRestoreRestrictedItem() restored policy group successfully", admin_policy.getGroup().getName(), restoredPolicy.getGroup().getName());
        assertEquals("testRestoreRestrictedItem() restored policy action successfully", admin_policy.getAction(), restoredPolicy.getAction());
        assertEquals("testRestoreRestrictedItem() restored policy name successfully", admin_policy.getRpName(), restoredPolicy.getRpName());

        log.info("testRestoreRestrictedItem() - END");
    }

    /**
     * Test restoration from AIP of an Item that has no access policies associated with it.
     */
    @Test
    public void testRestoreItemNoPolicies() throws Exception
    {
        new NonStrictExpectations(authorizeService.getClass())
        {{
            // Allow Full Admin permissions. Since we are working with an object
            // hierarchy  (Items/Bundles/Bitstreams) you need full admin rights
            authorizeService.isAdmin((Context) any); result = true;
        }};

        log.info("testRestoreItemNoPolicies() - BEGIN");

        // Locate the test Collection (as a parent)
        Collection parent = (Collection) handleService.resolveToObject(context, testCollectionHandle);

        // Create a brand new Item to test with (since we will be changing policies)
        WorkspaceItem wsItem = workspaceItemService.create(context, parent, false);
        Item item = installItemService.installItem(context, wsItem);
        itemService.addMetadata(context, item, "dc", "title", null, null, "Test No Policies Item");
        // Create a test Bitstream in the ORIGINAL bundle
        File f = new File(testProps.get("test.bitstream").toString());
        Bitstream b = itemService.createSingleBitstream(context, new FileInputStream(f), item);
        b.setName(context, "Test Bitstream");
        bitstreamService.update(context, b);
        itemService.update(context, item);

        // Remove all existing policies from the Item
        authorizeService.removeAllPolicies(context, item);

        // Export item AIP
        log.info("testRestoreItemNoPolicies() - CREATE Item AIP");
        File aipFile = createAIP(item, null, false);

        // Get item handle, so we can check that it is later restored properly
        String itemHandle = item.getHandle();

        // Now, delete that item
        log.info("testRestoreItemNoPolicies() - DELETE Item");
        collectionService.removeItem(context, parent, item);

        // Assert the deleted item no longer exists
        DSpaceObject obj = handleService.resolveToObject(context, itemHandle);
        assertThat("testRestoreItemNoPolicies() item " + itemHandle + " doesn't exist", obj, nullValue());

        // Restore Item from AIP (non-recursive)
        log.info("testRestoreItemNoPolicies() - RESTORE Item");
        restoreFromAIP(parent, aipFile, null, false);

        // Assert the deleted item is RESTORED
        DSpaceObject objRestored = handleService.resolveToObject(context, itemHandle);
        assertThat("testRestoreItemNoPolicies() item " + itemHandle + " exists", objRestored, notNullValue());

        // Assert the restored item also has ZERO policies
        List<ResourcePolicy> policiesRestored = authorizeService.getPolicies(context, objRestored);
        assertEquals("testRestoreItemNoPolicies() restored policy count is zero", 0, policiesRestored.size());

        log.info("testRestoreItemNoPolicies() - END");
    }

    /**
     * Test replacement from AIP of an Item object
     */
    @Test
    public void testReplaceItem() throws Exception
    {
        new NonStrictExpectations(authorizeService.getClass())
        {{
            // Allow Full Admin permissions. Since we are working with an object
            // hierarchy (Items/Bundles/Bitstreams) you need full admin rights
            authorizeService.isAdmin((Context) any); result = true;
        }};
        
        log.info("testReplaceItem() - BEGIN");

        // Locate the item (from our test data)
        Item testItem = (Item) handleService.resolveToObject(context, testItemHandle);
        
        // Get its current name / title
        String oldName = testItem.getName();
        
        // Export item AIP
        log.info("testReplaceItem() - CREATE Item AIP");
        File aipFile = createAIP(testItem, null, false);
        
        // Change the Item name
        String newName = "This is NOT my Item name!";
        itemService.clearMetadata(context, testItem, MetadataSchema.DC_SCHEMA, "title", null, Item.ANY);
        itemService.addMetadata(context, testItem, MetadataSchema.DC_SCHEMA, "title", null, null, newName);

        // Ensure name is changed
        assertEquals("testReplaceItem() new name", testItem.getName(), newName);
        
        // Now, replace our Item from AIP (non-recursive)
        replaceFromAIP(testItem, aipFile, null, false);

        // Check if name reverted to previous value
        assertEquals("testReplaceItem() old name", testItem.getName(), oldName);
    }
    
    /**
     * Test restoration from AIP of an Item that is mapped to multiple Collections.
     * This tests restoring the mapped Item FROM its own AIP
     */
    @Test
    public void testRestoreMappedItem() throws Exception
    {
        new NonStrictExpectations(authorizeService.getClass())
        {{
            // Allow Full Admin permissions. Since we are working with an object
            // hierarchy  (Items/Bundles/Bitstreams) you need full admin rights
            authorizeService.isAdmin((Context) any); result = true;
            authorizeService.authorizeAction(context, (Collection) any, Constants.REMOVE); result = null;
            authorizeService.authorizeAction(context, (Collection) any, Constants.ADD); result = null;
        }};

        log.info("testRestoreMappedItem() - BEGIN");

        // Get a reference to our test mapped Item
        Item item = (Item) handleService.resolveToObject(context, testMappedItemHandle);
        // Get owning Collection
        Collection owner = item.getOwningCollection();
        
        // Assert that it is in multiple collections
        List<Collection> mappedCollections = item.getCollections();
        assertEquals("testRestoreMappedItem() item " + testMappedItemHandle + " is mapped to multiple collections", 2, mappedCollections.size());
        
        // Export mapped item AIP
        log.info("testRestoreMappedItem() - CREATE Mapped Item AIP");
        File aipFile = createAIP(item, null, false);
        
        // Now, delete that item (must be removed from BOTH collections to delete it)
        log.info("testRestoreMappedItem() - DELETE Item");
        itemService.delete(context, item);

        // Assert the deleted item no longer exists
        DSpaceObject obj = handleService.resolveToObject(context, testMappedItemHandle);
        assertThat("testRestoreMappedItem() item " + testMappedItemHandle + " doesn't exist", obj, nullValue());
        
        // Restore Item from AIP (non-recursive) into its original parent collection
        log.info("testRestoreMappedItem() - RESTORE Item");
        restoreFromAIP(owner, aipFile, null, false);
        // Commit these changes to our DB

        // Assert the deleted item is RESTORED
        Item itemRestored = (Item) handleService.resolveToObject(context, testMappedItemHandle);
        assertThat("testRestoreMappedItem() item " + testMappedItemHandle + " exists", itemRestored, notNullValue());
        
        // Test that this restored Item exists in multiple Collections
        List<Collection> restoredMappings = itemRestored.getCollections();
        assertEquals("testRestoreMappedItem() collection count", 2, restoredMappings.size());
        
        log.info("testRestoreMappedItem() - END");
    }
    
    /**
     * Create AIP(s) based on a given DSpaceObject. This is a simple utility method
     * to avoid having to rewrite this code into several tests.
     * @param dso DSpaceObject to create AIP(s) for
     * @param pkParams any special PackageParameters to pass (if any)
     * @param recursive whether to recursively create AIPs or just a single AIP
     * @return exported root AIP file
     */
    private File createAIP(DSpaceObject dso, PackageParameters pkgParams, boolean recursive)
            throws PackageException, CrosswalkException, AuthorizeException, SQLException, IOException
    {
        // Get a reference to the configured "AIP" package disseminator
        PackageDisseminator dip = (PackageDisseminator) pluginService
                    .getNamedPlugin(PackageDisseminator.class, "AIP");
        if (dip == null)
        {
            fail("Could not find a disseminator for type 'AIP'");
            return null;
        }
        else
        {
            // Export file (this is placed in JUnit's temporary folder, so that it can be cleaned up after tests complete)
            File exportAIPFile = new File(aipTempFolder.getRoot().getAbsolutePath() + File.separator + PackageUtils.getPackageName(dso, "zip"));

            // If unspecified, set default PackageParameters
            if (pkgParams==null)
                pkgParams = new PackageParameters();

            // Actually disseminate the object(s) to AIPs
            if(recursive)
                dip.disseminateAll(context, dso, pkgParams, exportAIPFile);
            else
                dip.disseminate(context, dso, pkgParams, exportAIPFile);

            return exportAIPFile;
        }
    }
    
    /**
     * Restore DSpaceObject(s) from AIP(s). This is a simple utility method
     * to avoid having to rewrite this code into several tests.
     * @param parent The DSpaceObject which will be the parent object of the newly restored object(s)
     * @param aipFile AIP file to start restoration from
     * @param pkParams any special PackageParameters to pass (if any)
     * @param recursive whether to recursively restore AIPs or just a single AIP
     */
    private void restoreFromAIP(DSpaceObject parent, File aipFile, PackageParameters pkgParams, boolean recursive)
            throws PackageException, CrosswalkException, AuthorizeException, SQLException, IOException, WorkflowException {
        // Get a reference to the configured "AIP" package ingestor
        PackageIngester sip = (PackageIngester) pluginService
                    .getNamedPlugin(PackageIngester.class, "AIP");
        if(sip == null)
        {
            fail("Could not find a ingestor for type 'AIP'");
        }
        else
        {
            if(!aipFile.exists())
            {
                fail("AIP Package File does NOT exist: " + aipFile.getAbsolutePath());
            }

            // If unspecified, set default PackageParameters
            if(pkgParams==null)
                pkgParams = new PackageParameters();

            // Ensure restore mode is enabled
            pkgParams.setRestoreModeEnabled(true);

            // Actually ingest the object(s) from AIPs
            if(recursive)
                sip.ingestAll(context, parent, aipFile, pkgParams, null);
            else
                sip.ingest(context, parent, aipFile, pkgParams, null);
        }
    }
    
    /**
     * Replace DSpaceObject(s) from AIP(s). This is a simple utility method
     * to avoid having to rewrite this code into several tests.
     * @param dso The DSpaceObject to be replaced from AIP
     * @param aipFile AIP file to start replacement from
     * @param pkParams any special PackageParameters to pass (if any)
     * @param recursive whether to recursively restore AIPs or just a single AIP
     */
    private void replaceFromAIP(DSpaceObject dso, File aipFile, PackageParameters pkgParams, boolean recursive)
            throws PackageException, CrosswalkException, AuthorizeException, SQLException, IOException, WorkflowException {
        // Get a reference to the configured "AIP" package ingestor
        PackageIngester sip = (PackageIngester) pluginService
                    .getNamedPlugin(PackageIngester.class, "AIP");
        if (sip == null)
        {
            fail("Could not find a ingestor for type 'AIP'");
        }
        else
        {
            if(!aipFile.exists())
            {
                fail("AIP Package File does NOT exist: " + aipFile.getAbsolutePath());
            }

            // If unspecified, set default PackageParameters
            if (pkgParams==null)
                pkgParams = new PackageParameters();

            // Ensure restore mode is enabled
            pkgParams.setRestoreModeEnabled(true);

            // Actually replace the object(s) from AIPs
            if(recursive)
                sip.replaceAll(context, dso, aipFile, pkgParams);
            else
                sip.replace(context, dso, aipFile, pkgParams);
        }
    }
    
    /**
     * Save Object hierarchy info to the given HashMap. This utility method can
     * be used in conjunction with "assertObjectsExist" and "assertObjectsNotExist"
     * methods below, in order to assert whether a restoration succeeded or not.
     * <P>
     * In HashMap, Key is the object handle, and Value is "[type-text]::[title]".
     * @param dso DSpaceObject
     * @param infoMap HashMap
     * @throws SQLException if database error
     */
    private void saveObjectInfo(DSpaceObject dso, HashMap<String,String> infoMap)
            throws SQLException
    {
        // We need the HashMap to be non-null
        if(infoMap==null)
            return;
        
        if(dso instanceof Community)
        {
            // Save this Community's info to the infoMap
            Community community = (Community) dso;
            infoMap.put(community.getHandle(), communityService.getTypeText(community) + valueseparator + community.getName());
            
            // Recursively call method for each SubCommunity
            List<Community> subCommunities = community.getSubcommunities();
            for(Community c : subCommunities)
            {
                saveObjectInfo(c, infoMap);
            }
            
            // Recursively call method for each Collection
            List<Collection> collections = community.getCollections();
            for(Collection c : collections)
            {
                saveObjectInfo(c, infoMap);
            }
        }
        else if(dso instanceof Collection)
        {
            // Save this Collection's info to the infoMap
            Collection collection = (Collection) dso;
            infoMap.put(collection.getHandle(), collectionService.getTypeText(collection) + valueseparator + collection.getName());
            
            // Recursively call method for each Item in Collection
            Iterator<Item> items = itemService.findByCollection(context, collection);
            while(items.hasNext())
            {
                Item i = items.next();
                saveObjectInfo(i, infoMap);
            }
        }
        else if(dso instanceof Item)
        {
            // Save this Item's info to the infoMap
            Item item = (Item) dso;
            infoMap.put(item.getHandle(), itemService.getTypeText(item) + valueseparator + item.getName());
        }
    }
    
    /**
     * Assert the objects listed in a HashMap all exist in DSpace and have 
     * properties equal to HashMap value(s).
     * <P>
     * In HashMap, Key is the object handle, and Value is "[type-text]::[title]".
     * @param infoMap HashMap of objects to check for
     * @throws SQLException if database error
     */
    private void assertObjectsExist(HashMap<String,String> infoMap)
            throws SQLException
    {
        if(infoMap==null || infoMap.isEmpty())
            fail("Cannot assert against an empty infoMap");
        
        // Loop through everything in infoMap, and ensure it all exists
        for(String key : infoMap.keySet())
        {
            // The Key is the Handle, so make sure this object exists
            DSpaceObject obj = handleService.resolveToObject(context, key);
            assertThat("assertObjectsExist object " + key + " (info=" + infoMap.get(key) + ") exists", obj, notNullValue());
            
            // Get the typeText & name of this object from the values
            String info = infoMap.get(key);
            String[] values = info.split(valueseparator);
            String typeText = values[0];
            String name = values[1];
            
            // Also assert type and name are correct
            assertEquals("assertObjectsExist object " + key + " type", ContentServiceFactory.getInstance().getDSpaceObjectService(obj).getTypeText(obj), typeText);
            assertEquals("assertObjectsExist object " + key + " name", obj.getName(), name);
        }
        
    }
    
    /**
     * Assert the objects listed in a HashMap do NOT exist in DSpace.
     * @param infoMap HashMap of objects to check for
     * @throws SQLException if database error
     */
    public void assertObjectsNotExist(HashMap<String,String> infoMap)
            throws SQLException
    {
        if(infoMap==null || infoMap.isEmpty())
            fail("Cannot assert against an empty infoMap");
        
        // Loop through everything in infoMap, and ensure it all exists
        for(String key : infoMap.keySet())
        {
            // The key is the Handle, so make sure this object does NOT exist
            DSpaceObject obj = handleService.resolveToObject(context, key);
            assertThat("assertObjectsNotExist object " + key + " (info=" + infoMap.get(key) + ") doesn't exist", obj, nullValue());
        }
    }
}
