org.openmrs.module.auditlog.CollectionsAuditLogBehaviorTest.java Source code

Java tutorial

Introduction

Here is the source code for org.openmrs.module.auditlog.CollectionsAuditLogBehaviorTest.java

Source

/**
 * The contents of this file are subject to the OpenMRS Public License
 * Version 1.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://license.openmrs.org
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 *
 * Copyright (C) OpenMRS, LLC.  All Rights Reserved.
 */
package org.openmrs.module.auditlog;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.openmrs.module.auditlog.AuditLog.Action.CREATED;
import static org.openmrs.module.auditlog.AuditLog.Action.DELETED;
import static org.openmrs.module.auditlog.AuditLog.Action.UPDATED;
import static org.openmrs.module.auditlog.util.AuditLogConstants.MAP_KEY_VALUE_SEPARATOR;
import static org.openmrs.module.auditlog.util.AuditLogConstants.SEPARATOR;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.codehaus.jackson.map.ObjectMapper;
import org.hibernate.persister.collection.CollectionPersister;
import org.junit.Test;
import org.openmrs.Concept;
import org.openmrs.ConceptDescription;
import org.openmrs.Location;
import org.openmrs.LocationTag;
import org.openmrs.Order;
import org.openmrs.Patient;
import org.openmrs.PatientIdentifier;
import org.openmrs.PatientProgram;
import org.openmrs.Person;
import org.openmrs.PersonAddress;
import org.openmrs.PersonAttribute;
import org.openmrs.PersonName;
import org.openmrs.Relationship;
import org.openmrs.Role;
import org.openmrs.User;
import org.openmrs.api.LocationService;
import org.openmrs.api.OrderService;
import org.openmrs.api.PatientService;
import org.openmrs.api.PersonService;
import org.openmrs.api.ProgramWorkflowService;
import org.openmrs.api.UserService;
import org.openmrs.api.context.Context;
import org.openmrs.module.auditlog.strategy.AuditStrategy;
import org.openmrs.module.auditlog.util.AuditLogConstants;
import org.openmrs.module.auditlog.util.AuditLogUtil;
import org.springframework.test.annotation.NotTransactional;

/**
 * Contains tests for testing the core functionality of the module
 */
@SuppressWarnings("deprecation")
public class CollectionsAuditLogBehaviorTest extends BaseBehaviorTest {

    private List<String> getAsList(String str) throws Exception {
        return new ObjectMapper().readValue(str, List.class);
    }

    @Test
    @NotTransactional
    public void shouldCreateAnAuditLogForAParentWhenAnAuditedElementIsRemovedFromAChildCollection()
            throws Exception {
        PatientService ps = Context.getPatientService();
        Patient patient = ps.getPatient(2);
        ps.savePatient(patient);
        List<AuditLog> existingUpdateLogs = getAllLogs(patient.getId(), Patient.class,
                Collections.singletonList(UPDATED));
        assertEquals(0, existingUpdateLogs.size());
        int originalCount = patient.getNames().size();
        assertTrue(originalCount > 1);

        startAuditing(Patient.class);
        startAuditing(PersonName.class);
        assertTrue(auditLogService.isAudited(PersonName.class));
        PersonName nameToRemove = null;
        for (PersonName name : patient.getNames()) {
            if (!name.isPreferred()) {
                nameToRemove = name;
                break;
            }
        }
        assertNotNull(nameToRemove);
        Integer nameId = nameToRemove.getId();
        patient.removeName(nameToRemove);
        ps.savePatient(patient);
        assertEquals(originalCount - 1, patient.getNames().size());
        List<AuditLog> patientLogs = getAllLogs(patient.getId(), Patient.class, Collections.singletonList(UPDATED));
        assertEquals(1, patientLogs.size());
        AuditLog al = patientLogs.get(0);
        assertEquals(originalCount - 1, patient.getNames().size());
        assertFalse(getAsList(AuditLogUtil.getNewValueOfUpdatedItem("names", al))
                .contains(nameToRemove.getId().toString()));
        List<String> previousNameIds = getAsList(AuditLogUtil.getPreviousValueOfUpdatedItem("names", al));
        for (PersonName name : patient.getNames()) {
            assertTrue(previousNameIds.contains(name.getId().toString()));
        }

        List<AuditLog> nameLogs = getAllLogs(nameId, PersonName.class, Collections.singletonList(DELETED));
        assertEquals(1, nameLogs.size());
        assertEquals(al, nameLogs.get(0).getParentAuditLog());
    }

    @Test
    @NotTransactional
    public void shouldCreateAnAuditLogForAParentWhenAnAuditedElementIsAddedToAChildCollection() throws Exception {
        Concept concept = conceptService.getConcept(5089);
        //something with ConceptMaps having blank uuids and now getting set, this should have been
        //fixed in later versions
        conceptService.saveConcept(concept);
        List<AuditLog> existingUpdateLogs = getAllLogs(concept.getId(), Concept.class,
                Collections.singletonList(UPDATED));
        int originalCount = concept.getDescriptions().size();
        assertTrue(originalCount == 1);
        Integer previousDescriptionId = concept.getDescription().getId();

        startAuditing(ConceptDescription.class);
        assertTrue(auditLogService.isAudited(ConceptDescription.class));

        ConceptDescription cd1 = new ConceptDescription("desc1", Locale.ENGLISH);
        cd1.setDateCreated(new Date());
        cd1.setCreator(Context.getAuthenticatedUser());
        concept.addDescription(cd1);
        conceptService.saveConcept(concept);

        List<AuditLog> conceptLogs = getAllLogs(concept.getId(), Concept.class, Collections.singletonList(UPDATED));
        assertEquals(existingUpdateLogs.size() + 1, conceptLogs.size());
        conceptLogs.removeAll(existingUpdateLogs);
        assertEquals(1, conceptLogs.size());
        AuditLog al = conceptLogs.get(0);
        assertEquals(AuditLogUtil.getNewValueOfUpdatedItem("descriptions", al),
                "[\"" + previousDescriptionId + "\"" + AuditLogConstants.SEPARATOR + "\"" + cd1.getId() + "\"]");
        assertEquals(AuditLogUtil.getPreviousValueOfUpdatedItem("descriptions", al),
                "[\"" + previousDescriptionId + "\"]");

        List<AuditLog> descriptionLogs = getAllLogs(cd1.getId(), ConceptDescription.class,
                Collections.singletonList(CREATED));
        assertEquals(1, descriptionLogs.size());
        assertEquals(al, descriptionLogs.get(0).getParentAuditLog());
    }

    @Test
    @NotTransactional
    public void shouldCreateAnAuditLogForTheParentObjectWhenAnAuditedElementInAChildCollectionIsUpdated()
            throws Exception {
        Concept concept = conceptService.getConcept(7);
        assertEquals(0, getAllLogs(concept.getId(), Concept.class, Collections.singletonList(UPDATED)).size());

        int originalDescriptionCount = concept.getDescriptions().size();
        assertTrue(originalDescriptionCount > 0);

        startAuditing(ConceptDescription.class);
        assertTrue(auditLogService.isAudited(ConceptDescription.class));

        ConceptDescription description = concept.getDescription();
        description.setDescription("another descr");
        concept = conceptService.saveConcept(concept);
        assertEquals(originalDescriptionCount, concept.getDescriptions().size());
        List<AuditLog> conceptLogs = getAllLogs(concept.getId(), Concept.class, Collections.singletonList(UPDATED));
        assertEquals(1, conceptLogs.size());
        AuditLog al = conceptLogs.get(0);

        List<AuditLog> descriptionLogs = getAllLogs(description.getId(), ConceptDescription.class,
                Collections.singletonList(UPDATED));
        assertEquals(1, descriptionLogs.size());
        assertEquals(al, descriptionLogs.get(0).getParentAuditLog());
    }

    @Test
    @NotTransactional
    public void shouldCreateAnAuditLogForAParentWhenAnUnAuditedElementIsRemovedFromAChildCollection()
            throws Exception {
        assertFalse(auditLogService.isAudited(PersonName.class));
        PatientService ps = Context.getPatientService();
        Patient patient = ps.getPatient(2);
        ps.savePatient(patient);
        List<AuditLog> existingUpdateLogs = getAllLogs(patient.getId(), Patient.class,
                Collections.singletonList(UPDATED));
        assertEquals(0, existingUpdateLogs.size());
        int originalCount = patient.getNames().size();
        assertTrue(originalCount > 1);

        startAuditing(Patient.class);
        PersonName nameToRemove = null;
        for (PersonName name : patient.getNames()) {
            if (!name.isPreferred()) {
                nameToRemove = name;
                break;
            }
        }
        assertNotNull(nameToRemove);
        Integer nameId = nameToRemove.getId();
        patient.removeName(nameToRemove);
        ps.savePatient(patient);
        assertEquals(originalCount - 1, patient.getNames().size());
        List<AuditLog> patientLogs = getAllLogs(patient.getId(), Patient.class, Collections.singletonList(UPDATED));
        assertEquals(1, patientLogs.size());
        AuditLog al = patientLogs.get(0);
        assertEquals(originalCount - 1, patient.getNames().size());
        assertFalse(getAsList(AuditLogUtil.getNewValueOfUpdatedItem("names", al))
                .contains(nameToRemove.getId().toString()));
        List<String> previousNameIds = getAsList(AuditLogUtil.getPreviousValueOfUpdatedItem("names", al));
        for (PersonName name : patient.getNames()) {
            assertTrue(previousNameIds.contains(name.getId().toString()));
        }

        List<AuditLog> nameLogs = getAllLogs(nameId, PersonName.class, Collections.singletonList(DELETED));
        assertEquals(1, nameLogs.size());
        assertEquals(al, nameLogs.get(0).getParentAuditLog());
    }

    @Test
    @NotTransactional
    public void shouldCreateAnAuditLogForAParentWhenAnUnAuditedElementIsAddedToAChildCollection() throws Exception {
        assertFalse(auditLogService.isAudited(ConceptDescription.class));
        Concept concept = conceptService.getConcept(5089);
        //something with ConceptMaps having blank uuids and now getting set, this should have been
        //fixed in later versions
        conceptService.saveConcept(concept);
        List<AuditLog> existingUpdateLogs = getAllLogs(concept.getId(), Concept.class,
                Collections.singletonList(UPDATED));
        int originalCount = concept.getDescriptions().size();
        assertTrue(originalCount == 1);
        Integer previousDescriptionId = concept.getDescription().getId();

        ConceptDescription cd1 = new ConceptDescription("desc1", Locale.ENGLISH);
        cd1.setDateCreated(new Date());
        cd1.setCreator(Context.getAuthenticatedUser());
        concept.addDescription(cd1);
        conceptService.saveConcept(concept);

        List<AuditLog> conceptLogs = getAllLogs(concept.getId(), Concept.class, Collections.singletonList(UPDATED));
        assertEquals(existingUpdateLogs.size() + 1, conceptLogs.size());
        conceptLogs.removeAll(existingUpdateLogs);
        assertEquals(1, conceptLogs.size());
        AuditLog al = conceptLogs.get(0);
        assertEquals(AuditLogUtil.getNewValueOfUpdatedItem("descriptions", al),
                "[\"" + previousDescriptionId + "\"" + AuditLogConstants.SEPARATOR + "\"" + cd1.getId() + "\"]");
        assertEquals(AuditLogUtil.getPreviousValueOfUpdatedItem("descriptions", al),
                "[\"" + previousDescriptionId + "\"]");

        List<AuditLog> descriptionLogs = getAllLogs(cd1.getId(), ConceptDescription.class,
                Collections.singletonList(CREATED));
        assertEquals(1, descriptionLogs.size());
        assertEquals(al, descriptionLogs.get(0).getParentAuditLog());
    }

    @Test
    @NotTransactional
    public void shouldCreateAnAuditLogForTheParentObjectWhenAnUnAuditedElementInAChildCollectionIsUpdated()
            throws Exception {
        assertFalse(auditLogService.isAudited(ConceptDescription.class));
        Concept concept = conceptService.getConcept(7);
        assertEquals(0, getAllLogs(concept.getId(), Concept.class, Collections.singletonList(UPDATED)).size());

        int originalDescriptionCount = concept.getDescriptions().size();
        assertTrue(originalDescriptionCount > 0);

        ConceptDescription description = concept.getDescription();
        description.setDescription("another descr");
        concept = conceptService.saveConcept(concept);
        assertEquals(originalDescriptionCount, concept.getDescriptions().size());
        List<AuditLog> conceptLogs = getAllLogs(concept.getId(), Concept.class, Collections.singletonList(UPDATED));
        assertEquals(1, conceptLogs.size());
        AuditLog al = conceptLogs.get(0);

        List<AuditLog> descriptionLogs = getAllLogs(description.getId(), ConceptDescription.class,
                Collections.singletonList(UPDATED));
        assertEquals(1, descriptionLogs.size());
        assertEquals(al, descriptionLogs.get(0).getParentAuditLog());
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    @Test
    @NotTransactional
    public void shouldCreateAnAuditLogForAParentWhenAllItemsAreRemovedFromACollection() throws Exception {
        Concept c = conceptService.getConcept(7);
        List actions = Collections.singletonList(UPDATED);
        int count = getAllLogs(c.getId(), Concept.class, actions).size();
        assertEquals(4, c.getDescriptions().size());
        Iterator<ConceptDescription> it = c.getDescriptions().iterator();
        Integer descriptionId1 = it.next().getId();
        Integer descriptionId2 = it.next().getId();
        Integer descriptionId3 = it.next().getId();
        Integer descriptionId4 = it.next().getId();
        c.getDescriptions().clear();
        conceptService.saveConcept(c);

        List<AuditLog> logs = getAllLogs(c.getId(), Concept.class, actions);
        int newCount = logs.size();
        assertEquals(++count, newCount);
        AuditLog log = logs.get(0);
        assertNull(AuditLogUtil.getNewValueOfUpdatedItem("descriptions", log));
        assertNotNull(AuditLogUtil.getPreviousValueOfUpdatedItem("descriptions", log).indexOf(descriptionId1) > -1);
        assertNotNull(AuditLogUtil.getPreviousValueOfUpdatedItem("descriptions", log).indexOf(descriptionId2) > -1);
        assertNotNull(AuditLogUtil.getPreviousValueOfUpdatedItem("descriptions", log).indexOf(descriptionId3) > -1);
        assertNotNull(AuditLogUtil.getPreviousValueOfUpdatedItem("descriptions", log).indexOf(descriptionId4) > -1);
    }

    @Test
    @NotTransactional
    public void shouldCreateLogForUnAuditedTypeIfTheOwningTypeIsAuditedAndStrategyIsAllExcept() throws Exception {
        setAuditConfiguration(AuditStrategy.ALL_EXCEPT, null, false);
        assertEquals(AuditStrategy.ALL_EXCEPT, auditLogService.getAuditingStrategy());
        assertEquals(true, auditLogService.isAudited(Person.class));
        assertEquals(true, auditLogService.isAudited(PersonAddress.class));

        stopAuditing(PersonAddress.class);
        assertEquals(false, auditLogService.isAudited(PersonAddress.class));
        PersonService ps = Context.getPersonService();
        Person person = ps.getPerson(2);
        PersonAddress address = person.getPersonAddress();
        address.setAddress1("new");
        ps.savePerson(person);
        List<AuditLog> addressLogs = getAllLogs(address.getId(), PersonAddress.class,
                Collections.singletonList(UPDATED));
        assertEquals(1, addressLogs.size());
        List<AuditLog> personLogs = getAllLogs(person.getId(), Person.class, Collections.singletonList(UPDATED));
        assertEquals(1, personLogs.size());
        assertEquals(personLogs.get(0), addressLogs.get(0).getParentAuditLog());
    }

    @Test
    @NotTransactional
    public void shouldLinkTheLogsOfCollectionItemsToThatOfTheUpdatedParent() throws Exception {
        PatientService ps = Context.getPatientService();
        Patient patient = ps.getPatient(2);
        Set<Class<?>> classes = new HashSet<Class<?>>();
        classes.add(PersonName.class);
        classes.add(Patient.class);

        startAuditing(classes);
        patient = ps.savePatient(patient);
        //Ensure that no log will be created unless we actually perform an update
        assertEquals(0, getAllLogs(patient.getId(), Patient.class, Collections.singletonList(UPDATED)).size());

        int originalNameCount = patient.getNames().size();
        assertTrue(originalNameCount > 3);

        assertTrue(auditLogService.isAudited(PersonName.class));
        assertTrue(auditLogService.isAudited(Patient.class));
        Iterator<PersonName> it = patient.getNames().iterator();
        //update some existing names
        PersonName name1 = it.next();
        name1.setGivenName("another given name1");
        PersonName name2 = it.next();
        name2.setGivenName("another given name2");
        //remove the next 2
        PersonName name3 = it.next();
        it.remove();
        PersonName name4 = it.next();
        it.remove();
        PersonName name5 = new PersonName("another given name5", null, "another family name5");
        name5.setUuid("6e9226f4-999d-11e2-a6ac-b499bae1ce4e");
        name5.setDateCreated(new Date());
        name5.setCreator(Context.getAuthenticatedUser());
        PersonName name6 = new PersonName("another given name6", null, "another family name6");
        name6.setUuid("781f01b0-999d-11e2-a6ac-b499bae1ce4e");
        name6.setDateCreated(new Date());
        name6.setCreator(Context.getAuthenticatedUser());
        patient.addName(name5);
        patient.addName(name6);
        patient = ps.savePatient(patient);

        List<AuditLog> personNameAuditLogs1 = getAllLogs(name1.getId(), PersonName.class,
                Collections.singletonList(UPDATED));
        assertEquals(1, personNameAuditLogs1.size());
        AuditLog personNameAuditLog1 = personNameAuditLogs1.get(0);
        assertNotNull(personNameAuditLog1.getParentAuditLog());

        List<AuditLog> personNameAuditLogs2 = getAllLogs(name2.getId(), PersonName.class,
                Collections.singletonList(UPDATED));
        assertEquals(1, personNameAuditLogs2.size());
        AuditLog personNameAuditLog2 = personNameAuditLogs2.get(0);
        assertNotNull(personNameAuditLog2.getParentAuditLog());

        List<AuditLog> personNameAuditLogs3 = getAllLogs(name3.getId(), PersonName.class,
                Collections.singletonList(DELETED));
        assertEquals(1, personNameAuditLogs3.size());
        AuditLog personNameAuditLog3 = personNameAuditLogs3.get(0);
        assertNotNull(personNameAuditLog3.getParentAuditLog());

        List<AuditLog> personNameAuditLogs4 = getAllLogs(name4.getId(), PersonName.class,
                Collections.singletonList(DELETED));
        assertEquals(1, personNameAuditLogs4.size());
        AuditLog personNameAuditLog4 = personNameAuditLogs4.get(0);
        assertNotNull(personNameAuditLog4.getParentAuditLog());

        List<AuditLog> personNameAuditLogs5 = getAllLogs(name5.getId(), PersonName.class,
                Collections.singletonList(CREATED));
        assertEquals(1, personNameAuditLogs5.size());
        AuditLog personNameAuditLog5 = personNameAuditLogs5.get(0);
        assertNotNull(personNameAuditLog5.getParentAuditLog());

        List<AuditLog> personNameAuditLogs6 = getAllLogs(name6.getId(), PersonName.class,
                Collections.singletonList(CREATED));
        assertEquals(1, personNameAuditLogs6.size());
        AuditLog personNameAuditLog6 = personNameAuditLogs6.get(0);
        assertNotNull(personNameAuditLog6.getParentAuditLog());

        List<AuditLog> patientAuditLogs = getAllLogs(patient.getId(), Patient.class,
                Collections.singletonList(UPDATED));
        assertEquals(1, patientAuditLogs.size());
        assertEquals(6, patientAuditLogs.get(0).getChildAuditLogs().size());

        assertEquals(patientAuditLogs.get(0), personNameAuditLog1.getParentAuditLog());
        assertEquals(patientAuditLogs.get(0), personNameAuditLog2.getParentAuditLog());
        assertEquals(patientAuditLogs.get(0), personNameAuditLog3.getParentAuditLog());
        assertEquals(patientAuditLogs.get(0), personNameAuditLog4.getParentAuditLog());
        assertEquals(patientAuditLogs.get(0), personNameAuditLog5.getParentAuditLog());
        assertEquals(patientAuditLogs.get(0), personNameAuditLog6.getParentAuditLog());
    }

    @Test
    @NotTransactional
    public void shouldNotLinkTheLogsOfCollectionItemsToThatOfTheUpdatedParentIfCascadeOptionIsNotDeleteOrphan()
            throws Exception {
        //TODO Add check that the cascade options is not delete option
        Concept concept = conceptService.getConcept(7);
        assertEquals(0, getAllLogs(concept.getId(), Concept.class, Collections.singletonList(UPDATED)).size());
        int originalDescriptionCount = concept.getDescriptions().size();
        assertTrue(originalDescriptionCount > 3);

        startAuditing(ConceptDescription.class);
        concept = conceptService.saveConcept(concept);
        //Ensure that no log will be created unless we actually perform an update
        assertEquals(0, getAllLogs(concept.getId(), Concept.class, Collections.singletonList(UPDATED)).size());
        assertTrue(auditLogService.isAudited(ConceptDescription.class));
        Iterator<ConceptDescription> it = concept.getDescriptions().iterator();
        //update some existing descriptions
        ConceptDescription cd1 = it.next();
        cd1.setDescription("another descr1");
        ConceptDescription cd2 = it.next();
        cd2.setDescription("another descr2");
        //remove the next 2
        ConceptDescription cd3 = it.next();
        it.remove();
        ConceptDescription cd4 = it.next();
        it.remove();
        ConceptDescription cd5 = new ConceptDescription("yes in japanese", Locale.JAPANESE);
        cd5.setUuid("6e9226f4-999d-11e2-a6ac-b499bae1ce4e");
        cd5.setDateCreated(new Date());
        cd5.setCreator(Context.getAuthenticatedUser());
        ConceptDescription cd6 = new ConceptDescription("yes in chinese", Locale.CHINESE);
        cd6.setUuid("781f01b0-999d-11e2-a6ac-b499bae1ce4e");
        cd6.setDateCreated(new Date());
        cd6.setCreator(Context.getAuthenticatedUser());
        concept.addDescription(cd5);
        concept.addDescription(cd6);
        concept = conceptService.saveConcept(concept);
        assertEquals(originalDescriptionCount, concept.getDescriptions().size());
        List<AuditLog> descriptionAuditLogs1 = getAllLogs(cd1.getId(), ConceptDescription.class,
                Collections.singletonList(UPDATED));
        assertEquals(1, descriptionAuditLogs1.size());
        AuditLog descriptionAuditLog1 = descriptionAuditLogs1.get(0);
        assertNotNull(descriptionAuditLog1.getParentAuditLog());

        List<AuditLog> descriptionAuditLogs2 = getAllLogs(cd2.getId(), ConceptDescription.class,
                Collections.singletonList(UPDATED));
        assertEquals(1, descriptionAuditLogs2.size());
        AuditLog descriptionAuditLog2 = descriptionAuditLogs2.get(0);
        assertNotNull(descriptionAuditLog2.getParentAuditLog());

        List<AuditLog> descriptionAuditLogs3 = getAllLogs(cd3.getId(), ConceptDescription.class,
                Collections.singletonList(DELETED));
        //This is because concept.descriptions cascade option doesn't say delete-orphan
        assertEquals(0, descriptionAuditLogs3.size());

        List<AuditLog> descriptionAuditLogs4 = getAllLogs(cd4.getId(), ConceptDescription.class,
                Collections.singletonList(DELETED));
        assertEquals(0, descriptionAuditLogs4.size());//same here

        List<AuditLog> descriptionAuditLogs5 = getAllLogs(cd5.getId(), ConceptDescription.class,
                Collections.singletonList(CREATED));
        assertEquals(1, descriptionAuditLogs5.size());
        AuditLog descriptionAuditLog5 = descriptionAuditLogs5.get(0);
        assertNotNull(descriptionAuditLog5.getParentAuditLog());

        List<AuditLog> descriptionAuditLogs6 = getAllLogs(cd6.getId(), ConceptDescription.class,
                Collections.singletonList(CREATED));
        assertEquals(1, descriptionAuditLogs6.size());
        AuditLog descriptionAuditLog6 = descriptionAuditLogs6.get(0);
        assertNotNull(descriptionAuditLog6.getParentAuditLog());

        List<AuditLog> conceptAuditLogs = getAllLogs(concept.getId(), Concept.class,
                Collections.singletonList(UPDATED));
        assertEquals(1, conceptAuditLogs.size());
        assertEquals(4, conceptAuditLogs.get(0).getChildAuditLogs().size());

        assertEquals(conceptAuditLogs.get(0), descriptionAuditLog1.getParentAuditLog());
        assertEquals(conceptAuditLogs.get(0), descriptionAuditLog2.getParentAuditLog());
    }

    @Test
    @NotTransactional
    public void shouldLinkTheLogsOfCollectionItemsToThatOfTheDeletedParent() throws Exception {
        PatientService ps = Context.getPatientService();
        Patient patient = ps.getPatient(2);
        Set<Class<?>> classes = new HashSet<Class<?>>();
        classes.add(PersonName.class);
        classes.add(PersonAttribute.class);
        classes.add(PersonAddress.class);
        classes.add(PatientIdentifier.class);
        classes.add(Patient.class);

        startAuditing(classes);
        patient = ps.savePatient(patient);
        //Ensure that no log will be created unless we actually perform an update
        assertEquals(0, getAllLogs(patient.getId(), Patient.class, Collections.singletonList(UPDATED)).size());
        assertTrue(auditLogService.isAudited(PersonName.class));
        assertTrue(auditLogService.isAudited(Patient.class));

        assertEquals(4, patient.getNames().size());
        Iterator<PersonName> nameIt = patient.getNames().iterator();
        PersonName name1 = nameIt.next();
        PersonName name2 = nameIt.next();
        PersonName name3 = nameIt.next();
        PersonName name4 = nameIt.next();

        assertEquals(1, patient.getAddresses().size());
        PersonAddress address = patient.getAddresses().iterator().next();

        assertEquals(2, patient.getIdentifiers().size());
        Iterator<PatientIdentifier> idIt = patient.getIdentifiers().iterator();
        PatientIdentifier identifier1 = idIt.next();
        PatientIdentifier identifier2 = idIt.next();

        assertEquals(3, patient.getAttributes().size());
        Iterator<PersonAttribute> attributeIt = patient.getAttributes().iterator();
        PersonAttribute attribute1 = attributeIt.next();
        PersonAttribute attribute2 = attributeIt.next();
        PersonAttribute attribute3 = attributeIt.next();

        PersonService personService = Context.getPersonService();
        List<Relationship> relationships = personService.getRelationshipsByPerson(patient);
        for (Relationship r : relationships) {
            personService.purgeRelationship(r);
        }
        OrderService os = Context.getOrderService();
        List<Order> orders = os.getOrdersByPatient(patient);
        for (Order o : orders) {
            os.purgeOrder(o);
        }
        ProgramWorkflowService pws = Context.getProgramWorkflowService();
        List<PatientProgram> pps = pws.getPatientPrograms(patient, null, null, null, null, null, true);
        for (PatientProgram pp : pps) {
            pws.purgePatientProgram(pp);
        }
        ps.purgePatient(patient);

        List<AuditLog> personName1AuditLogs = getAllLogs(name1.getId(), PersonName.class,
                Collections.singletonList(DELETED));
        assertEquals(1, personName1AuditLogs.size());
        AuditLog personName1AuditLog = personName1AuditLogs.get(0);
        assertNotNull(personName1AuditLog.getParentAuditLog());

        List<AuditLog> personName2AuditLogs = getAllLogs(name2.getId(), PersonName.class,
                Collections.singletonList(DELETED));
        assertEquals(1, personName2AuditLogs.size());
        AuditLog personName2AuditLog = personName2AuditLogs.get(0);
        assertNotNull(personName2AuditLog.getParentAuditLog());

        List<AuditLog> personName3AuditLogs = getAllLogs(name3.getId(), PersonName.class,
                Collections.singletonList(DELETED));
        assertEquals(1, personName3AuditLogs.size());
        AuditLog personName3AuditLog = personName3AuditLogs.get(0);
        assertNotNull(personName3AuditLog.getParentAuditLog());

        List<AuditLog> personName4AuditLogs = getAllLogs(name4.getId(), PersonName.class,
                Collections.singletonList(DELETED));
        assertEquals(1, personName4AuditLogs.size());
        AuditLog personName4AuditLog = personName4AuditLogs.get(0);
        assertNotNull(personName4AuditLog.getParentAuditLog());

        List<AuditLog> addressAuditLogs = getAllLogs(address.getId(), PersonAddress.class,
                Collections.singletonList(DELETED));
        assertEquals(1, addressAuditLogs.size());
        AuditLog addressAuditLog = addressAuditLogs.get(0);
        assertNotNull(addressAuditLog.getParentAuditLog());

        List<AuditLog> id1AuditLogs = getAllLogs(identifier1.getId(), PatientIdentifier.class,
                Collections.singletonList(DELETED));
        assertEquals(1, id1AuditLogs.size());
        AuditLog id1AuditLog = id1AuditLogs.get(0);
        assertNotNull(id1AuditLog.getParentAuditLog());

        List<AuditLog> id2AuditLogs = getAllLogs(identifier2.getId(), PatientIdentifier.class,
                Collections.singletonList(DELETED));
        assertEquals(1, id2AuditLogs.size());
        AuditLog id2AuditLog = id2AuditLogs.get(0);
        assertNotNull(id2AuditLog.getParentAuditLog());

        List<AuditLog> attribute1AuditLogs = getAllLogs(attribute1.getId(), PersonAttribute.class,
                Collections.singletonList(DELETED));
        assertEquals(1, attribute1AuditLogs.size());
        AuditLog attribute1AuditLog = attribute1AuditLogs.get(0);
        assertNotNull(attribute1AuditLog.getParentAuditLog());

        List<AuditLog> attribute2AuditLogs = getAllLogs(attribute2.getId(), PersonAttribute.class,
                Collections.singletonList(DELETED));
        assertEquals(1, attribute2AuditLogs.size());
        AuditLog attribute2AuditLog = attribute2AuditLogs.get(0);
        assertNotNull(attribute2AuditLog.getParentAuditLog());

        List<AuditLog> attribute3AuditLogs = getAllLogs(attribute3.getId(), PersonAttribute.class,
                Collections.singletonList(DELETED));
        assertEquals(1, attribute3AuditLogs.size());
        AuditLog attribute3AuditLog = attribute3AuditLogs.get(0);
        assertNotNull(attribute3AuditLog.getParentAuditLog());

        List<AuditLog> patientAuditLogs = getAllLogs(patient.getId(), Patient.class,
                Collections.singletonList(DELETED));
        assertEquals(1, patientAuditLogs.size());
        assertEquals(10, patientAuditLogs.get(0).getChildAuditLogs().size());

        assertEquals(patientAuditLogs.get(0), personName1AuditLog.getParentAuditLog());
        assertEquals(patientAuditLogs.get(0), personName2AuditLog.getParentAuditLog());
        assertEquals(patientAuditLogs.get(0), personName3AuditLog.getParentAuditLog());
        assertEquals(patientAuditLogs.get(0), personName4AuditLog.getParentAuditLog());
        assertEquals(patientAuditLogs.get(0), addressAuditLog.getParentAuditLog());
        assertEquals(patientAuditLogs.get(0), id1AuditLog.getParentAuditLog());
        assertEquals(patientAuditLogs.get(0), id2AuditLog.getParentAuditLog());
        assertEquals(patientAuditLogs.get(0), attribute1AuditLog.getParentAuditLog());
        assertEquals(patientAuditLogs.get(0), attribute2AuditLog.getParentAuditLog());
        assertEquals(patientAuditLogs.get(0), attribute3AuditLog.getParentAuditLog());
    }

    @Test
    @NotTransactional
    public void shouldCreateAnAuditLogForTheParentWhenAnEntryIsAddedToAMapProperty() throws Exception {
        executeDataSet("org/openmrs/api/include/UserServiceTest.xml");
        UserService us = Context.getUserService();
        User user = us.getUser(505);
        assertEquals(1, user.getUserProperties().size());
        Map.Entry<String, String> entry = user.getUserProperties().entrySet().iterator().next();
        String previousUserProperties = "\"" + entry.getKey() + "\"" + MAP_KEY_VALUE_SEPARATOR + "\""
                + entry.getValue() + "\"";
        assertEquals(0, getAllLogs(user.getId(), User.class, null).size());
        startAuditing(User.class);
        assertEquals(true, auditLogService.isAudited(User.class));
        final String newPropKey1 = "locale";
        final String newPropValue1 = "fr";
        final String newPropKey2 = "loginAttempts";
        final String newPropValue2 = "2";
        user.setUserProperty(newPropKey1, newPropValue1);
        user.setUserProperty(newPropKey2, newPropValue2);
        //Should work even for detached owners
        Context.evictFromSession(user);
        us.saveUser(user, null);
        List<AuditLog> logs = getAllLogs(user.getId(), User.class, null);
        assertEquals(1, logs.size());
        AuditLog al = logs.get(0);
        assertEquals("{" + previousUserProperties + "}",
                AuditLogUtil.getPreviousValueOfUpdatedItem("userProperties", al));
        String expectedNewUserProperties = "{" + previousUserProperties + SEPARATOR + "\"" + newPropKey2 + "\""
                + MAP_KEY_VALUE_SEPARATOR + "\"" + newPropValue2 + "\"" + SEPARATOR + "\"" + newPropKey1 + "\""
                + MAP_KEY_VALUE_SEPARATOR + "\"" + newPropValue1 + "\"}";
        assertEquals(expectedNewUserProperties, AuditLogUtil.getNewValueOfUpdatedItem("userProperties", al));
    }

    @Test
    @NotTransactional
    public void shouldCreateAnAuditLogForTheParentWhenAnEntryIsRemovedFromAMapProperty() throws Exception {
        executeDataSet("org/openmrs/api/include/UserServiceTest.xml");
        UserService us = Context.getUserService();
        User user = us.getUser(505);
        assertEquals(1, user.getUserProperties().size());
        Map.Entry<String, String> entry = user.getUserProperties().entrySet().iterator().next();
        String previousUserProperties = "{\"" + entry.getKey() + "\"" + MAP_KEY_VALUE_SEPARATOR + "\""
                + entry.getValue() + "\"}";
        assertEquals(0, getAllLogs(user.getId(), User.class, null).size());
        startAuditing(User.class);
        assertEquals(true, auditLogService.isAudited(User.class));
        user.getUserProperties().clear();//since it is 1, just clear
        us.saveUser(user, null);
        List<AuditLog> logs = getAllLogs(user.getId(), User.class, null);
        assertEquals(1, logs.size());
        AuditLog al = logs.get(0);
        assertEquals(previousUserProperties, AuditLogUtil.getPreviousValueOfUpdatedItem("userProperties", al));
        assertNull(AuditLogUtil.getNewValueOfUpdatedItem("userProperties", al));
    }

    @Test
    @NotTransactional
    public void shouldCreateAnAuditLogForTheParentWhenAMapPropertyIsReplacedWithANewInstance() throws Exception {
        executeDataSet("org/openmrs/api/include/UserServiceTest.xml");
        UserService us = Context.getUserService();
        User user = us.getUser(505);
        assertEquals(1, user.getUserProperties().size());
        Map<String, String> originalProperties = user.getUserProperties();
        Map.Entry<String, String> entry = originalProperties.entrySet().iterator().next();
        String previousUserProperties = "{\"" + entry.getKey() + "\"" + MAP_KEY_VALUE_SEPARATOR + "\""
                + entry.getValue() + "\"}";
        assertEquals(0, getAllLogs(user.getId(), User.class, null).size());
        startAuditing(User.class);
        assertEquals(true, auditLogService.isAudited(User.class));
        Map<String, String> newProperties = new HashMap<String, String>();
        final String newKey = "this is new";
        final String newValue = "this is the value";
        newProperties.put(newKey, newValue);
        user.setUserProperties(newProperties);
        //Should work even for detached owners
        Context.evictFromSession(user);
        us.saveUser(user, null);
        List<AuditLog> logs = getAllLogs(user.getId(), User.class, null);
        assertEquals(1, logs.size());
        AuditLog al = logs.get(0);
        assertEquals(previousUserProperties, AuditLogUtil.getPreviousValueOfUpdatedItem("userProperties", al));
        assertEquals("{\"" + newKey + "\":\"" + newValue + "\"}",
                AuditLogUtil.getNewValueOfUpdatedItem("userProperties", al));
    }

    @Test
    @NotTransactional
    public void shouldCreateAnAuditLogForTheParentWhenAMapPropertyIsReplacedWithANullValue() throws Exception {
        executeDataSet("org/openmrs/api/include/UserServiceTest.xml");
        UserService us = Context.getUserService();
        User user = us.getUser(505);
        assertEquals(1, user.getUserProperties().size());
        Map<String, String> originalProperties = user.getUserProperties();
        Map.Entry<String, String> entry = originalProperties.entrySet().iterator().next();
        String previousUserProperties = "{\"" + entry.getKey() + "\"" + MAP_KEY_VALUE_SEPARATOR + "\""
                + entry.getValue() + "\"}";
        assertEquals(0, getAllLogs(user.getId(), User.class, null).size());
        startAuditing(User.class);
        assertEquals(true, auditLogService.isAudited(User.class));
        user.setUserProperties(null);
        //Should work even for detached owners
        Context.evictFromSession(user);
        us.saveUser(user, null);
        List<AuditLog> logs = getAllLogs(user.getId(), User.class, null);
        assertEquals(1, logs.size());
        AuditLog al = logs.get(0);
        assertEquals(previousUserProperties, AuditLogUtil.getPreviousValueOfUpdatedItem("userProperties", al));
        assertNull(AuditLogUtil.getNewValueOfUpdatedItem("userProperties", al));
    }

    @Test
    @NotTransactional
    public void shouldCreateAnAuditLogWhenNoChangesHaveBeenMadeToAUser() throws Exception {
        executeDataSet("org/openmrs/api/include/UserServiceTest.xml");
        UserService us = Context.getUserService();
        User user = us.getUser(505);
        assertEquals(1, user.getUserProperties().size());
        final String key = "some key";
        final String value = "some value";
        assertEquals(key, user.getUserProperties().keySet().iterator().next());
        assertEquals(value, user.getUserProperties().values().iterator().next());
        assertEquals(0, getAllLogs(user.getId(), User.class, null).size());
        startAuditing(User.class);
        assertEquals(true, auditLogService.isAudited(User.class));
        //We are setting the same original value but the string are new objects
        user.setUserProperty(key, value);
        us.saveUser(user, null);
        List<AuditLog> logs = getAllLogs(user.getId(), User.class, null);
        assertEquals(0, logs.size());
    }

    @Test
    @NotTransactional
    public void shouldSerializeMapEntriesAsSerializedDataForADeletedItem() throws Exception {
        executeDataSet("org/openmrs/api/include/UserServiceTest.xml");
        AuditLogUtil.setGlobalProperty(AuditLogConstants.GP_STORE_LAST_STATE_OF_DELETED_ITEMS, "true");
        UserService us = Context.getUserService();
        User user = us.getUser(505);
        assertEquals(1, user.getUserProperties().size());
        assertEquals(0, getAllLogs(user.getId(), User.class, null).size());
        startAuditing(User.class);
        Map.Entry<String, String> entry = user.getUserProperties().entrySet().iterator().next();
        String userProperties = "{\"" + entry.getKey() + "\"" + MAP_KEY_VALUE_SEPARATOR + "\"" + entry.getValue()
                + "\"}";
        assertEquals(true, auditLogService.isAudited(User.class));
        us.purgeUser(user);
        List<AuditLog> logs = getAllLogs(user.getId(), User.class, Collections.singletonList(DELETED));
        assertEquals(1, logs.size());
        AuditLog al = logs.get(0);
        String serializedData = AuditLogUtil.getAsString(al.getSerializedData());
        Map<String, Object> propertyNameValueMap = new ObjectMapper().readValue(serializedData, Map.class);
        assertEquals(userProperties, propertyNameValueMap.get("userProperties"));
    }

    /**
     * This tests assumes that the owner has other changes
     * 
     * @throws Exception
     */
    @Test
    @NotTransactional
    public void shouldNotLinkChildLogsToThatOfAnEditedOwnerForUpdatedItemsInACollectionMappedAsManyToMany()
            throws Exception {
        UserService us = Context.getUserService();
        User user = us.getUser(501);
        assertEquals(1, user.getRoles().size());
        Role role = user.getRoles().iterator().next();
        assertEquals(0, getAllLogs(user.getId(), User.class, null).size());
        assertEquals(0, getAllLogs(role.getRole(), Role.class, null).size());
        CollectionPersister cp = AuditLogUtil.getCollectionPersister("roles", User.class, null);
        assertTrue(cp.isManyToMany());
        assertEquals(false, auditLogService.isAudited(User.class));
        assertEquals(false, auditLogService.isAudited(Role.class));

        startAuditing(User.class);
        startAuditing(Role.class);
        assertEquals(true, auditLogService.isAudited(User.class));
        assertEquals(true, auditLogService.isAudited(Role.class));

        user.setUsername("new user name");
        role.setDescription("Testing");
        us.saveUser(user, null);

        List<AuditLog> userLogs = getAllLogs(user.getId(), User.class, Collections.singletonList(UPDATED));
        assertEquals(1, userLogs.size());
        assertEquals(0, userLogs.get(0).getChildAuditLogs().size());
        List<AuditLog> roleLogs = getAllLogs(role.getRole(), Role.class, Collections.singletonList(UPDATED));
        assertEquals(1, roleLogs.size());
        assertNull(roleLogs.get(0).getParentAuditLog());
    }

    /**
     * This tests assumes that the owner has no other changes
     * 
     * @throws Exception
     */
    @Test
    @NotTransactional
    public void shouldNotMarkOwnerAsUpdatedWhenItemIsUpdatedAndIsInACollectionMappedAsManyToMany()
            throws Exception {
        UserService us = Context.getUserService();
        User user = us.getUser(501);
        assertEquals(1, user.getRoles().size());
        Role role = user.getRoles().iterator().next();
        assertEquals(0, getAllLogs(role.getRole(), Role.class, null).size());
        CollectionPersister cp = AuditLogUtil.getCollectionPersister("roles", User.class, null);
        assertTrue(cp.isManyToMany());
        assertEquals(false, auditLogService.isAudited(User.class));
        assertEquals(false, auditLogService.isAudited(Role.class));

        startAuditing(User.class);
        startAuditing(Role.class);
        assertEquals(true, auditLogService.isAudited(User.class));
        assertEquals(true, auditLogService.isAudited(Role.class));

        role.setDescription("Testing");
        us.saveUser(user, null);

        assertEquals(0, getAllLogs(user.getId(), User.class, Collections.singletonList(UPDATED)).size());
        List<AuditLog> roleLogs = getAllLogs(role.getRole(), Role.class, Collections.singletonList(UPDATED));
        assertEquals(1, roleLogs.size());
        assertNull(roleLogs.get(0).getParentAuditLog());
    }

    /**
     * This tests assumes that the owner has no other changes
     * 
     * @throws Exception
     */
    @Test
    @NotTransactional
    public void shouldCreateLogForTheOwnerAndNotTheItemThatIsRemovedFromACollectionMappedAsManyToMany()
            throws Exception {
        UserService us = Context.getUserService();
        User user = us.getUser(501);
        assertEquals(1, user.getRoles().size());
        Role role = user.getRoles().iterator().next();
        assertEquals(0, getAllLogs(user.getId(), User.class, null).size());
        assertEquals(0, getAllLogs(role.getRole(), Role.class, null).size());
        CollectionPersister cp = AuditLogUtil.getCollectionPersister("roles", User.class, null);
        assertTrue(cp.isManyToMany());
        assertEquals(false, auditLogService.isAudited(User.class));

        startAuditing(User.class);
        assertEquals(true, auditLogService.isAudited(User.class));

        user.removeRole(role);
        us.saveUser(user, null);

        assertEquals(0, getAllLogs(role.getRole(), Role.class, null).size());
        //But should create an audit log for the owner
        List<AuditLog> userLogs = getAllLogs(user.getId(), User.class, Collections.singletonList(UPDATED));
        assertEquals(1, userLogs.size());
        AuditLog al = userLogs.get(0);
        assertEquals(0, al.getChildAuditLogs().size());
    }

    /**
     * This tests assumes that the owner has other changes
     * 
     * @throws Exception
     */
    @Test
    @NotTransactional
    public void shouldNotCreateLogForAnItemRemovedFromACollectionMappedAsManyToManyAndOwnerIsEdited()
            throws Exception {
        UserService us = Context.getUserService();
        User user = us.getUser(501);
        assertEquals(1, user.getRoles().size());
        Role role = user.getRoles().iterator().next();
        assertEquals(0, getAllLogs(user.getId(), User.class, null).size());
        assertEquals(0, getAllLogs(role.getRole(), Role.class, null).size());
        CollectionPersister cp = AuditLogUtil.getCollectionPersister("roles", User.class, null);
        assertTrue(cp.isManyToMany());
        assertEquals(false, auditLogService.isAudited(User.class));

        startAuditing(User.class);
        assertEquals(true, auditLogService.isAudited(User.class));

        user.setUsername("New");
        user.removeRole(role);
        us.saveUser(user, null);

        assertEquals(0, getAllLogs(role.getRole(), Role.class, null).size());
        //But should create an audit log for the owner
        List<AuditLog> userLogs = getAllLogs(user.getId(), User.class, Collections.singletonList(UPDATED));
        assertEquals(1, userLogs.size());
        AuditLog al = userLogs.get(0);
        assertEquals(0, al.getChildAuditLogs().size());
    }

    /**
     * This tests assumes that the owner has no other changes and the collection item is new
     * 
     * @throws Exception
     */
    @Test
    @NotTransactional
    public void shouldNotLinkLogsToThatOfTheOwnerForANewItemAddedToACollectionMappedAsManyToMany()
            throws Exception {
        UserService us = Context.getUserService();
        User user = us.getUser(501);
        assertEquals(1, user.getRoles().size());
        assertEquals(0, getAllLogs(user.getId(), User.class, null).size());
        CollectionPersister cp = AuditLogUtil.getCollectionPersister("roles", User.class, null);
        assertTrue(cp.isManyToMany());
        assertEquals(false, auditLogService.isAudited(User.class));
        assertEquals(false, auditLogService.isAudited(Role.class));

        startAuditing(User.class);
        startAuditing(Role.class);
        assertEquals(true, auditLogService.isAudited(User.class));
        assertEquals(true, auditLogService.isAudited(Role.class));

        Role role = new Role("new role", "new desc");
        user.addRole(role);
        us.saveUser(user, null);

        List<AuditLog> roleLogs = getAllLogs(role.getRole(), Role.class, Collections.singletonList(CREATED));
        assertEquals(1, roleLogs.size());
        List<AuditLog> userLogs = getAllLogs(user.getId(), User.class, Collections.singletonList(UPDATED));
        assertEquals(1, userLogs.size());
        AuditLog al = userLogs.get(0);
        assertEquals(0, al.getChildAuditLogs().size());
        assertNull(roleLogs.get(0).getParentAuditLog());
    }

    /**
     * This tests assumes that the owner has no other changes and collection item exists
     * 
     * @throws Exception
     */
    @Test
    @NotTransactional
    public void shouldNotLinkLogsToThatOfTheOwnerForAnExistingItemAddedToACollectionMappedAsManyToMany()
            throws Exception {
        UserService us = Context.getUserService();
        User user = us.getUser(501);
        Role role = us.getRole("Anonymous");
        assertNotNull(role);
        assertFalse(user.getRoles().contains(role));
        assertEquals(0, getAllLogs(user.getId(), User.class, null).size());
        CollectionPersister cp = AuditLogUtil.getCollectionPersister("roles", User.class, null);
        assertTrue(cp.isManyToMany());
        assertEquals(false, auditLogService.isAudited(User.class));

        startAuditing(User.class);
        assertEquals(true, auditLogService.isAudited(User.class));

        user.addRole(role);
        us.saveUser(user, null);

        assertEquals(0, getAllLogs(role.getRole(), Role.class, null).size());
        List<AuditLog> userLogs = getAllLogs(user.getId(), User.class, Collections.singletonList(UPDATED));
        assertEquals(1, userLogs.size());
        assertEquals(0, userLogs.get(0).getChildAuditLogs().size());
    }

    /**
     * This tests assumes that the owner has other changes and collection item is new
     * 
     * @throws Exception
     */
    @Test
    @NotTransactional
    public void shouldNotLinkLogsToThatOfTheOwnerForANewItemAddedToACollectionMappedAsManyToManyAndOwnerIsEdited()
            throws Exception {
        UserService us = Context.getUserService();
        User user = us.getUser(501);
        assertEquals(1, user.getRoles().size());
        assertEquals(0, getAllLogs(user.getId(), User.class, null).size());
        CollectionPersister cp = AuditLogUtil.getCollectionPersister("roles", User.class, null);
        assertTrue(cp.isManyToMany());
        assertEquals(false, auditLogService.isAudited(User.class));
        assertEquals(false, auditLogService.isAudited(Role.class));

        startAuditing(User.class);
        startAuditing(Role.class);
        assertEquals(true, auditLogService.isAudited(User.class));
        assertEquals(true, auditLogService.isAudited(Role.class));

        user.setUsername("New");
        Role role = new Role("new role", "new desc");
        user.addRole(role);
        us.saveUser(user, null);

        List<AuditLog> roleLogs = getAllLogs(role.getRole(), Role.class, Collections.singletonList(CREATED));
        assertEquals(1, roleLogs.size());
        List<AuditLog> userLogs = getAllLogs(user.getId(), User.class, Collections.singletonList(UPDATED));
        assertEquals(1, userLogs.size());
        AuditLog al = userLogs.get(0);
        assertEquals(0, al.getChildAuditLogs().size());
        assertNull(roleLogs.get(0).getParentAuditLog());
    }

    /**
     * This tests assumes that the owner has other changes and collection item exists
     * 
     * @throws Exception
     */
    @Test
    @NotTransactional
    public void shouldNotLinkLogsToThatOfTheOwnerForAnExistingItemAddedToACollectionMappedAsManyToManyAndOwnerIsEdited()
            throws Exception {
        UserService us = Context.getUserService();
        User user = us.getUser(501);
        Role role = us.getRole("Anonymous");
        assertNotNull(role);
        assertFalse(user.getRoles().contains(role));
        assertEquals(0, getAllLogs(user.getId(), User.class, null).size());
        CollectionPersister cp = AuditLogUtil.getCollectionPersister("roles", User.class, null);
        assertTrue(cp.isManyToMany());
        assertEquals(false, auditLogService.isAudited(User.class));

        startAuditing(User.class);
        assertEquals(true, auditLogService.isAudited(User.class));

        user.setUsername("New");
        user.addRole(role);
        us.saveUser(user, null);

        assertEquals(0, getAllLogs(role.getRole(), Role.class, null).size());
        List<AuditLog> userLogs = getAllLogs(user.getId(), User.class, Collections.singletonList(UPDATED));
        assertEquals(1, userLogs.size());
        assertEquals(0, userLogs.get(0).getChildAuditLogs().size());
    }

    /**
     * For this test we remove the item by replacing the collection with a nw instance that doesn't
     * contain it
     * 
     * @throws Exception
     */
    @Test
    @NotTransactional
    public void shouldMarkTheParentAsEditedWhenAnItemIsRemovedFromACollectionByReplacingTheCollectionInstance()
            throws Exception {
        executeDataSet("org/openmrs/api/include/LocationServiceTest-initialData.xml");
        LocationService ls = Context.getLocationService();
        Location location = ls.getLocation(2);
        ls.saveLocation(location);
        List<AuditLog> existingUpdateLogs = getAllLogs(location.getId(), Location.class,
                Collections.singletonList(UPDATED));
        assertEquals(0, existingUpdateLogs.size());
        assertEquals(2, location.getTags().size());

        startAuditing(Location.class);
        startAuditing(LocationTag.class);
        assertTrue(auditLogService.isAudited(Location.class));
        assertTrue(auditLogService.isAudited(LocationTag.class));
        Iterator<LocationTag> i = location.getTags().iterator();
        LocationTag tagToRemove = i.next();
        Integer tagId = tagToRemove.getId();
        Collection<LocationTag> originalColl = location.getTags();
        LocationTag keptTag = i.next();
        location.setTags(Collections.singleton(keptTag));

        ls.saveLocation(location);

        assertTrue(originalColl != location.getTags());
        assertEquals(1, location.getTags().size());
        assertFalse(location.getTags().contains(tagToRemove));
        List<AuditLog> patientLogs = getAllLogs(location.getId(), Location.class,
                Collections.singletonList(UPDATED));
        assertEquals(1, patientLogs.size());
        AuditLog al = patientLogs.get(0);
        assertEquals(0, al.getChildAuditLogs().size());
        assertFalse(getAsList(AuditLogUtil.getNewValueOfUpdatedItem("tags", al))
                .contains(tagToRemove.getId().toString()));
        assertTrue(
                getAsList(AuditLogUtil.getNewValueOfUpdatedItem("tags", al)).contains(keptTag.getId().toString()));
        List<String> previousTagIds = getAsList(AuditLogUtil.getPreviousValueOfUpdatedItem("tags", al));
        assertTrue(previousTagIds.contains(tagToRemove.getId().toString()));
        assertTrue(previousTagIds.contains(keptTag.getId().toString()));
        List<AuditLog> tagLogs = getAllLogs(tagId, LocationTag.class, null);
        assertEquals(0, tagLogs.size());
    }

    /**
     * For this test we remove the item by replacing the collection with a nw instance that doesn't
     * contain it
     * 
     * @throws Exception
     */
    @Test
    @NotTransactional
    public void shouldMarkTheParentAsEditedWhenAnItemIsAddedToACollectionByReplacingTheCollectionInstance()
            throws Exception {
        executeDataSet("org/openmrs/api/include/LocationServiceTest-initialData.xml");
        LocationService ls = Context.getLocationService();
        Location location = ls.getLocation(2);
        ls.saveLocation(location);
        List<AuditLog> existingUpdateLogs = getAllLogs(location.getId(), Location.class,
                Collections.singletonList(UPDATED));
        assertEquals(0, existingUpdateLogs.size());
        Set<LocationTag> previousTags = new HashSet<LocationTag>();
        previousTags.addAll(location.getTags());
        assertEquals(2, previousTags.size());

        startAuditing(Location.class);
        startAuditing(LocationTag.class);
        assertTrue(auditLogService.isAudited(Location.class));
        assertTrue(auditLogService.isAudited(LocationTag.class));
        LocationTag tagToAdd = ls.getLocationTag(2);
        assertFalse(location.getTags().contains(tagToAdd));
        Integer tagId = tagToAdd.getId();
        Collection<LocationTag> originalColl = location.getTags();
        location.setTags(Collections.singleton(tagToAdd));

        ls.saveLocation(location);

        assertTrue(originalColl != location.getTags());
        assertEquals(1, location.getTags().size());
        assertTrue(location.getTags().contains(tagToAdd));
        List<AuditLog> patientLogs = getAllLogs(location.getId(), Location.class,
                Collections.singletonList(UPDATED));
        assertEquals(1, patientLogs.size());
        AuditLog al = patientLogs.get(0);
        assertEquals(0, al.getChildAuditLogs().size());
        List<String> newTagIds = getAsList(AuditLogUtil.getNewValueOfUpdatedItem("tags", al));
        List<String> previousTagIds = getAsList(AuditLogUtil.getPreviousValueOfUpdatedItem("tags", al));
        assertTrue(newTagIds.contains(tagToAdd.getId().toString()));
        assertFalse(previousTagIds.contains(tagToAdd.getId().toString()));
        for (LocationTag tag : previousTags) {
            assertFalse(newTagIds.contains(tag.getId().toString()));
            assertTrue(previousTagIds.contains(tag.getId().toString()));
        }
        List<AuditLog> tagLogs = getAllLogs(tagId, LocationTag.class, null);
        assertEquals(0, tagLogs.size());
    }

    /**
     * For this test we remove the item by replacing the collection with a new instance that doesn't
     * contain it
     * 
     * @throws Exception
     */
    @Test
    @NotTransactional
    public void shouldMarkTheParentAsEditedWhenItemsAreAddedAndRemovedToAndFromACollectionByReplacingTheCollectionInstance()
            throws Exception {
        executeDataSet("org/openmrs/api/include/LocationServiceTest-initialData.xml");
        LocationService ls = Context.getLocationService();
        Location location = ls.getLocation(2);
        ls.saveLocation(location);
        List<AuditLog> existingUpdateLogs = getAllLogs(location.getId(), Location.class,
                Collections.singletonList(UPDATED));
        assertEquals(0, existingUpdateLogs.size());
        Set<LocationTag> previousTags = new HashSet<LocationTag>();
        previousTags.addAll(location.getTags());
        assertEquals(2, previousTags.size());

        startAuditing(Location.class);
        startAuditing(LocationTag.class);
        assertTrue(auditLogService.isAudited(Location.class));
        assertTrue(auditLogService.isAudited(LocationTag.class));
        Iterator<LocationTag> i = location.getTags().iterator();
        LocationTag tagToRemove = i.next();
        Integer tagToRemoveId = tagToRemove.getId();
        LocationTag keptTag = i.next();
        LocationTag tagToAdd = ls.getLocationTag(2);
        assertFalse(location.getTags().contains(tagToAdd));
        Integer tagToAddId = tagToAdd.getId();
        Set<LocationTag> newTags = new HashSet<LocationTag>();
        newTags.add(keptTag);
        newTags.add(tagToAdd);
        location.setTags(newTags);

        ls.saveLocation(location);

        assertTrue(previousTags != location.getTags());
        assertEquals(2, location.getTags().size());
        assertTrue(location.getTags().contains(keptTag));
        assertTrue(location.getTags().contains(tagToAdd));
        List<AuditLog> patientLogs = getAllLogs(location.getId(), Location.class,
                Collections.singletonList(UPDATED));
        assertEquals(1, patientLogs.size());
        AuditLog al = patientLogs.get(0);
        assertEquals(0, al.getChildAuditLogs().size());
        List<String> previousTagIds = getAsList(AuditLogUtil.getPreviousValueOfUpdatedItem("tags", al));
        List<String> newTagIds = getAsList(AuditLogUtil.getNewValueOfUpdatedItem("tags", al));
        assertFalse(newTagIds.contains(tagToRemove.getId().toString()));
        assertTrue(previousTagIds.contains(tagToRemove.getId().toString()));
        assertTrue(newTagIds.contains(keptTag.getId().toString()));
        assertTrue(previousTagIds.contains(keptTag.getId().toString()));

        assertTrue(newTagIds.contains(tagToAdd.getId().toString()));
        assertFalse(previousTagIds.contains(tagToAdd.getId().toString()));

        List<AuditLog> tagLogs = getAllLogs(tagToRemoveId, LocationTag.class, null);
        assertEquals(0, tagLogs.size());
        tagLogs = getAllLogs(tagToAddId, LocationTag.class, null);
        assertEquals(0, tagLogs.size());
        tagLogs = getAllLogs(keptTag.getId(), LocationTag.class, null);
        assertEquals(0, tagLogs.size());
    }

    @Test
    @NotTransactional
    public void shouldCreateAnAuditLogForADetachedParentWhenACollectionNotMappedAsAllDeleteOrphanIsUpdated()
            throws Exception {
        PatientService ps = Context.getPatientService();
        Patient patient = ps.getPatient(2);
        ps.savePatient(patient);
        List<AuditLog> existingUpdateLogs = getAllLogs(patient.getId(), Patient.class,
                Collections.singletonList(UPDATED));
        assertEquals(0, existingUpdateLogs.size());
        int originalCount = patient.getNames().size();
        assertTrue(originalCount > 1);

        startAuditing(Patient.class);
        startAuditing(PersonName.class);
        assertTrue(auditLogService.isAudited(PersonName.class));
        PersonName nameToRemove = null;
        for (PersonName name : patient.getNames()) {
            if (!name.isPreferred()) {
                nameToRemove = name;
                break;
            }
        }
        assertNotNull(nameToRemove);
        Integer nameId = nameToRemove.getId();
        patient.removeName(nameToRemove);
        Context.evictFromSession(patient);
        ps.savePatient(patient);
        assertEquals(originalCount - 1, patient.getNames().size());
        List<AuditLog> patientLogs = getAllLogs(patient.getId(), Patient.class, Collections.singletonList(UPDATED));
        assertEquals(1, patientLogs.size());
        AuditLog al = patientLogs.get(0);
        assertEquals(originalCount - 1, patient.getNames().size());
        assertFalse(getAsList(AuditLogUtil.getNewValueOfUpdatedItem("names", al))
                .contains(nameToRemove.getId().toString()));
        List<String> previousNameIds = getAsList(AuditLogUtil.getPreviousValueOfUpdatedItem("names", al));
        for (PersonName name : patient.getNames()) {
            assertTrue(previousNameIds.contains(name.getId().toString()));
        }

        List<AuditLog> nameLogs = getAllLogs(nameId, PersonName.class, Collections.singletonList(DELETED));
        assertEquals(1, nameLogs.size());
        assertEquals(al, nameLogs.get(0).getParentAuditLog());
    }

    /**
     * For this test we remove the item by replacing the collection with a new instance that doesn't
     * contain it
     * 
     * @throws Exception
     */
    @Test
    @NotTransactional
    public void shouldCreateAnAuditLogForADetachedParentWhenACollectionMappedAsAllDeleteOrphanIsCleared()
            throws Exception {
        executeDataSet("org/openmrs/api/include/LocationServiceTest-initialData.xml");
        LocationService ls = Context.getLocationService();
        Location location = ls.getLocation(2);
        ls.saveLocation(location);
        List<AuditLog> existingUpdateLogs = getAllLogs(location.getId(), Location.class,
                Collections.singletonList(UPDATED));
        assertEquals(0, existingUpdateLogs.size());
        Set<LocationTag> previousTags = new HashSet<LocationTag>();
        previousTags.addAll(location.getTags());
        assertEquals(2, previousTags.size());

        startAuditing(Location.class);
        startAuditing(LocationTag.class);
        assertTrue(auditLogService.isAudited(Location.class));
        assertTrue(auditLogService.isAudited(LocationTag.class));
        location.getTags().clear();
        Context.evictFromSession(location);
        ls.saveLocation(location);

        List<AuditLog> locationLogs = getAllLogs(location.getId(), Location.class,
                Collections.singletonList(UPDATED));
        assertEquals(1, locationLogs.size());
        AuditLog al = locationLogs.get(0);
        assertEquals(0, al.getChildAuditLogs().size());
        assertNull(AuditLogUtil.getNewValueOfUpdatedItem("tags", al));
        List<String> previousTagIds = getAsList(AuditLogUtil.getPreviousValueOfUpdatedItem("tags", al));
        for (LocationTag tag : previousTags) {
            assertTrue(previousTagIds.contains(tag.getId().toString()));
        }
        List<Class<?>> classes = new ArrayList<Class<?>>();
        classes.add(LocationTag.class);
        List<AuditLog.Action> actions = new ArrayList<AuditLog.Action>();
        actions.add(DELETED);
        List<AuditLog> logs = auditLogService.getAuditLogs(classes, actions, null, null, false, null, null);
        assertEquals(0, logs.size());
    }

    /**
     * For this test we remove the item by replacing the collection with a new instance that doesn't
     * contain it
     * 
     * @throws Exception
     */
    @Test
    @NotTransactional
    public void shouldCreateAnAuditLogForADetachedParentWhenACollectionMappedAsAllDeleteOrphanIsReplaceWithANewInstance()
            throws Exception {
        executeDataSet("org/openmrs/api/include/LocationServiceTest-initialData.xml");
        LocationService ls = Context.getLocationService();
        Location location = ls.getLocation(2);
        ls.saveLocation(location);
        List<AuditLog> existingUpdateLogs = getAllLogs(location.getId(), Location.class,
                Collections.singletonList(UPDATED));
        assertEquals(0, existingUpdateLogs.size());
        Set<LocationTag> previousTags = new HashSet<LocationTag>();
        previousTags.addAll(location.getTags());
        assertEquals(2, previousTags.size());

        startAuditing(Location.class);
        startAuditing(LocationTag.class);
        assertTrue(auditLogService.isAudited(Location.class));
        assertTrue(auditLogService.isAudited(LocationTag.class));
        location.setTags(new HashSet<LocationTag>());
        Context.evictFromSession(location);
        ls.saveLocation(location);

        List<AuditLog> patientLogs = getAllLogs(location.getId(), Location.class,
                Collections.singletonList(UPDATED));
        assertEquals(1, patientLogs.size());
        AuditLog al = patientLogs.get(0);
        assertEquals(0, al.getChildAuditLogs().size());
        assertNull(AuditLogUtil.getNewValueOfUpdatedItem("tags", al));
        List<String> previousTagIds = getAsList(AuditLogUtil.getPreviousValueOfUpdatedItem("tags", al));
        for (LocationTag tag : previousTags) {
            assertTrue(previousTagIds.contains(tag.getId().toString()));
        }
        List<Class<?>> classes = new ArrayList<Class<?>>();
        classes.add(LocationTag.class);
        List<AuditLog.Action> actions = new ArrayList<AuditLog.Action>();
        actions.add(DELETED);
        List<AuditLog> logs = auditLogService.getAuditLogs(classes, actions, null, null, false, null, null);
        assertEquals(0, logs.size());
    }

    /**
     * For this test we remove the item by replacing the collection with a new instance that doesn't
     * contain it
     * 
     * @throws Exception
     */
    @Test
    @NotTransactional
    public void shouldCreateAnAuditLogForADetachedParentWhenACollectionMappedAsAllDeleteOrphanIsRemovedBySettingItToNull()
            throws Exception {
        executeDataSet("org/openmrs/api/include/LocationServiceTest-initialData.xml");
        LocationService ls = Context.getLocationService();
        Location location = ls.getLocation(2);
        ls.saveLocation(location);
        List<AuditLog> existingUpdateLogs = getAllLogs(location.getId(), Location.class,
                Collections.singletonList(UPDATED));
        assertEquals(0, existingUpdateLogs.size());
        Set<LocationTag> previousTags = new HashSet<LocationTag>();
        previousTags.addAll(location.getTags());
        assertEquals(2, previousTags.size());

        startAuditing(Location.class);
        startAuditing(LocationTag.class);
        assertTrue(auditLogService.isAudited(Location.class));
        assertTrue(auditLogService.isAudited(LocationTag.class));
        location.setTags(null);
        Context.evictFromSession(location);
        ls.saveLocation(location);

        List<AuditLog> patientLogs = getAllLogs(location.getId(), Location.class,
                Collections.singletonList(UPDATED));
        assertEquals(1, patientLogs.size());
        AuditLog al = patientLogs.get(0);
        assertEquals(0, al.getChildAuditLogs().size());
        assertNull(AuditLogUtil.getNewValueOfUpdatedItem("tags", al));
        List<String> previousTagIds = getAsList(AuditLogUtil.getPreviousValueOfUpdatedItem("tags", al));
        for (LocationTag tag : previousTags) {
            assertTrue(previousTagIds.contains(tag.getId().toString()));
        }
        List<Class<?>> classes = new ArrayList<Class<?>>();
        classes.add(LocationTag.class);
        List<AuditLog.Action> actions = new ArrayList<AuditLog.Action>();
        actions.add(DELETED);
        List<AuditLog> logs = auditLogService.getAuditLogs(classes, actions, null, null, false, null, null);
        assertEquals(0, logs.size());
    }
}