org.jspresso.hrsample.backend.JspressoModelTest.java Source code

Java tutorial

Introduction

Here is the source code for org.jspresso.hrsample.backend.JspressoModelTest.java

Source

/*
 * Copyright (c) 2005-2016 Vincent Vandenschrick. All rights reserved.
 *
 *  This file is part of the Jspresso framework.
 *
 *  Jspresso is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Jspresso is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with Jspresso.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.jspresso.hrsample.backend;

import static org.junit.Assert.*;
import static org.mockito.Matchers.argThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.hamcrest.Description;
import org.hibernate.Hibernate;
import org.hibernate.SQLQuery;
import org.hibernate.collection.internal.PersistentSet;
import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.hibernate.sql.JoinType;
import org.junit.Test;
import org.mockito.ArgumentMatcher;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;

import org.jspresso.framework.application.backend.entity.ControllerAwareEntityInvocationHandler;
import org.jspresso.framework.application.backend.persistence.hibernate.HibernateBackendController;
import org.jspresso.framework.application.backend.session.EMergeMode;
import org.jspresso.framework.model.component.ComponentException;
import org.jspresso.framework.model.component.basic.ICollectionWrapper;
import org.jspresso.framework.model.descriptor.MandatoryPropertyException;
import org.jspresso.framework.model.entity.IEntity;
import org.jspresso.framework.model.persistence.hibernate.criterion.EnhancedDetachedCriteria;
import org.jspresso.framework.util.bean.integrity.IntegrityException;
import org.jspresso.framework.util.reflect.ReflectHelper;
import org.jspresso.framework.util.uid.ByteArray;

import org.jspresso.hrsample.model.City;
import org.jspresso.hrsample.model.Company;
import org.jspresso.hrsample.model.ContactInfo;
import org.jspresso.hrsample.model.Department;
import org.jspresso.hrsample.model.Employee;
import org.jspresso.hrsample.model.Event;
import org.jspresso.hrsample.model.Link;
import org.jspresso.hrsample.model.Nameable;
import org.jspresso.hrsample.model.OrganizationalUnit;
import org.jspresso.hrsample.model.Team;

/**
 * Model integration tests.
 *
 * @author Vincent Vandenschrick
 */
public class JspressoModelTest extends BackTestStartup {

    /**
     * Tests computed fire property change. See bug 708.
     *
     * @throws Throwable
     *     whenever an unexpected error occurs.
     */
    @Test
    public void testComputedFirePropertyChange() throws Throwable {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();
        boolean wasThrowExceptionOnBadUsage = hbc.isThrowExceptionOnBadUsage();
        try {
            hbc.setThrowExceptionOnBadUsage(false);
            EnhancedDetachedCriteria employeeCriteria = EnhancedDetachedCriteria.forClass(Employee.class);
            Employee employee = hbc.findFirstByCriteria(employeeCriteria, EMergeMode.MERGE_KEEP, Employee.class);
            ControllerAwareEntityInvocationHandler handlerSpy = (ControllerAwareEntityInvocationHandler) spy(
                    Proxy.getInvocationHandler(employee));
            Employee employeeMock = (Employee) Proxy.newProxyInstance(getClass().getClassLoader(),
                    new Class<?>[] { Employee.class }, handlerSpy);

            Method firePropertyChangeMethod = Employee.class.getMethod("firePropertyChange", String.class,
                    Object.class, Object.class);

            employeeMock.setBirthDate(new Date(0));
            verify(handlerSpy, never()).invoke(eq(employeeMock), eq(firePropertyChangeMethod),
                    argThat(new PropertyMatcher(Employee.AGE)));

            PropertyChangeListener fakeListener = new PropertyChangeListener() {

                @Override
                public void propertyChange(PropertyChangeEvent evt) {
                    // NO-OP
                }
            };

            employeeMock.addPropertyChangeListener(fakeListener);
            employeeMock.setBirthDate(new Date(1));
            verify(handlerSpy, times(1)).invoke(eq(employeeMock), eq(firePropertyChangeMethod),
                    argThat(new PropertyMatcher(Employee.AGE)));

            employeeMock.removePropertyChangeListener(fakeListener);
            employeeMock.setBirthDate(new Date(2));
            verify(handlerSpy, times(1)).invoke(eq(employeeMock), eq(firePropertyChangeMethod),
                    argThat(new PropertyMatcher(Employee.AGE)));

            employeeMock.addPropertyChangeListener(Employee.AGE, fakeListener);
            employeeMock.setBirthDate(new Date(3));
            verify(handlerSpy, times(2)).invoke(eq(employeeMock), eq(firePropertyChangeMethod),
                    argThat(new PropertyMatcher(Employee.AGE)));
        } finally {
            hbc.setThrowExceptionOnBadUsage(wasThrowExceptionOnBadUsage);
        }
    }

    /**
     * Tests adding twice the same entity to a list property. See bug 758.
     */
    @Test(expected = ComponentException.class)
    public void testAddToListTwice() {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();

        EnhancedDetachedCriteria employeeCriteria = EnhancedDetachedCriteria.forClass(Employee.class);
        final Employee emp = hbc.findFirstByCriteria(employeeCriteria, EMergeMode.MERGE_KEEP, Employee.class);

        Event evt = emp.getEvents().get(0);
        emp.addToEvents(evt);
        assertSame(emp.getEvents().get(0), evt);
        assertSame(emp.getEvents().get(emp.getEvents().size() - 1), evt);

        hbc.getTransactionTemplate().execute(new TransactionCallbackWithoutResult() {

            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                hbc.cloneInUnitOfWork(emp);
            }
        });
        hbc.reload(emp);
        assertSame(emp.getEvents().get(0), evt);
        assertSame(emp.getEvents().get(emp.getEvents().size() - 1), evt);
    }

    /**
     * Tests 1st level cache.
     */
    @Test
    public void testCache() {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();
        EnhancedDetachedCriteria crit = EnhancedDetachedCriteria.forClass(City.class);
        List<City> cities = hbc.findByCriteria(crit, EMergeMode.MERGE_KEEP, City.class);
        City firstCity = hbc.findById(cities.get(0).getId(), EMergeMode.MERGE_KEEP, City.class);
        assertSame(cities.get(0), firstCity);
    }

    /**
     * Tests EAGER uninitialized merge on null value.
     */
    @Test
    public void testUninitializedMerge() {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();
        EnhancedDetachedCriteria crit = EnhancedDetachedCriteria.forClass(Department.class);
        final Department d1 = hbc.findFirstByCriteria(crit, EMergeMode.MERGE_LAZY, Department.class);
        d1.straightSetProperty("company", null);

        Department d2 = hbc.getTransactionTemplate().execute(new TransactionCallback<Department>() {

            @Override
            public Department doInTransaction(TransactionStatus status) {
                Department d = (Department) hbc.getHibernateSession().get(Department.class, d1.getId());
                status.setRollbackOnly();
                return d;
            }
        });
        d2 = hbc.merge(d2, EMergeMode.MERGE_EAGER);
        assertSame(d1, d2);
    }

    /**
     * Component backref initialization.
     */
    @Test
    public void testComponentBackRef() {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();
        Company c = hbc.getEntityFactory().createEntityInstance(Company.class);
        assertSame(c, c.getContact().getOwningComponent());

        EnhancedDetachedCriteria crit = EnhancedDetachedCriteria.forClass(Department.class);
        final Department d = hbc.findFirstByCriteria(crit, EMergeMode.MERGE_LAZY, Department.class);
        assertSame(d, d.getContact().getOwningComponent());

        hbc.getTransactionTemplate().execute(new TransactionCallbackWithoutResult() {

            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                Department dClone = hbc.cloneInUnitOfWork(d);
                assertSame(dClone, dClone.getContact().getOwningComponent());
                status.setRollbackOnly();
            }
        });
    }

    /**
     * Creates a SQL select and use ID.
     */
    @Test
    public void testSelectById() {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();

        EnhancedDetachedCriteria crit = EnhancedDetachedCriteria.forClass(Department.class);
        final Department d = hbc.findFirstByCriteria(crit, EMergeMode.MERGE_LAZY, Department.class);

        SQLQuery query = hbc.getHibernateSession()
                .createSQLQuery("UPDATE DEPARTMENT SET NAME = 'Test' WHERE ID = :ID_PARAM");
        Object depId = d.getId();
        if (depId instanceof ByteArray) {
            depId = ((ByteArray) depId).getBytes();
        }
        query.setParameter("ID_PARAM", depId);
        assertEquals(1, query.executeUpdate());
    }

    /**
     * Tests the performance of adder when the collection is big.
     */
    @Test(timeout = 15000)
    public void testCollectionSetterPerf() {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();

        Company company = hbc.getEntityFactory().createEntityInstance(Company.class);
        // for (int i = 0; i < 5000; i++) {
        // Employee emp = hbc.getEntityFactory()
        // .createEntityInstance(Employee.class);
        // // The employee collection should be sorted by name
        // emp.setName(Integer.toHexString(emp.hashCode()));
        // company.addToEmployees(emp);
        // }
        Set<Employee> employees = new HashSet<>();
        for (int i = 0; i < 5000; i++) {
            Employee emp = hbc.getEntityFactory().createEntityInstance(Employee.class);
            // The employee collection should be sorted by name
            emp.setName(Integer.toHexString(emp.hashCode()));
            employees.add(emp);
        }
        company.setEmployees(employees);
    }

    /**
     * Tests entities memory consumption.
     *
     * @throws InterruptedException
     *     the interrupted exception
     */
    @Test
    public void testEntitiesMemoryConsumption() throws InterruptedException {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();
        System.gc();
        //Thread.sleep(2000);
        long start = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
        List<Employee> employees = new ArrayList<>();
        for (int i = 0; i < 5000; i++) {
            Employee emp = hbc.getEntityFactory().createEntityInstance(Employee.class);
            // The employee collection should be sorted by name
            //emp.setName(Integer.toHexString(emp.hashCode()));
            emp.straightSetProperty(IEntity.ID, null);
            employees.add(emp);
        }
        System.gc();
        //Thread.sleep(2000);
        long end = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
        System.out.println((end - start) / 1024 / 1024 + " MB used memory for " + employees.size()
                + " entities. This " + "means " + (end - start) / employees.size() + " B per entity.");
    }

    /**
     * Tests fix for bug #787.
     */
    @Test
    public void testBigDecimalScale() {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();

        Employee e1 = hbc.getEntityFactory().createEntityInstance(Employee.class);
        Employee e2 = hbc.getEntityFactory().createEntityInstance(Employee.class);

        e1.setSalary(new BigDecimal("4"));
        e2.setSalary(new BigDecimal("4.00"));
        assertEquals(e1.getSalary(), e2.getSalary());

        e1.setSalary(new BigDecimal("4.12352"));
        e2.setSalary(new BigDecimal("4.12437"));
        assertEquals(e1.getSalary(), e2.getSalary());
    }

    /**
     * Tests fix for bug #928.
     */
    @Test
    @SuppressWarnings("unchecked")
    public void testJoinOrderBy() {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();
        EnhancedDetachedCriteria crit = EnhancedDetachedCriteria.forClass(Department.class);

        DetachedCriteria companyCrit = crit.getSubCriteriaFor(crit, Department.COMPANY, JoinType.INNER_JOIN);
        companyCrit.add(Restrictions.eq(Nameable.NAME, "Design2See"));

        DetachedCriteria teamsCrit = crit.getSubCriteriaFor(crit, Department.TEAMS, JoinType.LEFT_OUTER_JOIN);
        teamsCrit.add(Restrictions.eq(OrganizationalUnit.OU_ID, "HR-001"));

        crit.addOrder(Order.desc(Nameable.NAME));
        crit.addOrder(Order.asc(IEntity.ID));

        List<Department> depts = hbc.findByCriteria(crit, null, Department.class);
        for (Department d : depts) {
            // force collection sorting.
            Set<Team> teams = d.getTeams();
            Set<?> innerSet;
            try {
                if (teams instanceof ICollectionWrapper<?>) {
                    teams = (Set<Team>) ((ICollectionWrapper) teams).getWrappedCollection();
                }
                innerSet = (Set<?>) ReflectHelper.getPrivateFieldValue(PersistentSet.class, "set", teams);
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
            assertTrue("innerSet is a LinkedHashSet", LinkedHashSet.class.isInstance(innerSet));
        }
    }

    /**
     * Tests fix for bug #920.
     */
    @Test
    public void testRemovedEntityIsClean() {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();
        final City c = hbc.getEntityFactory().createEntityInstance(City.class);
        c.setName("Remove");
        c.setZip("00000");

        assertTrue(!c.isPersistent());
        assertTrue(hbc.isDirty(c));

        hbc.getTransactionTemplate().execute(new TransactionCallbackWithoutResult() {

            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                hbc.registerForUpdate(hbc.cloneInUnitOfWork(c));
            }
        });

        assertTrue(c.isPersistent());
        assertTrue(!hbc.isDirty(c));

        hbc.getTransactionTemplate().execute(new TransactionCallbackWithoutResult() {

            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                City c1 = hbc.cloneInUnitOfWork(c);
                try {
                    hbc.cleanRelationshipsOnDeletion(c1, false);
                } catch (Exception ex) {
                    throw new RuntimeException(ex);
                }
            }
        });

        assertTrue("Entity is still considered persistent after deletion", !c.isPersistent());
        assertTrue("Entity is still considered dirty, although we can't do much with it", !hbc.isDirty(c));
    }

    /**
     * Tests that 3+ level nested property changes get notified.
     */
    @Test
    public void testSubNestedPropertyChange() {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();

        EnhancedDetachedCriteria crit = EnhancedDetachedCriteria.forClass(Department.class);
        final Department d = hbc.findFirstByCriteria(crit, EMergeMode.MERGE_LAZY, Department.class);
        final StringBuilder buff = new StringBuilder();
        d.addPropertyChangeListener(
                OrganizationalUnit.MANAGER + "." + Employee.CONTACT + "." + ContactInfo.CITY + "." + Nameable.NAME,
                new PropertyChangeListener() {

                    @Override
                    public void propertyChange(PropertyChangeEvent evt) {
                        buff.append(evt.getNewValue());
                    }
                });
        City currentCity = d.getManager().getContact().getCity();
        currentCity.setName("testSubNotif");
        assertEquals("Sub-nested notification did not arrive correctly", currentCity.getName(), buff.toString());
        buff.delete(0, buff.length());

        City newCity = hbc.getEntityFactory().createEntityInstance(City.class);
        newCity.setName("newSubNotif");
        newCity.setZip("12345");
        d.getManager().getContact().setCity(newCity);
        assertEquals("Sub-nested notification did not arrive correctly", newCity.getName(), buff.toString());
        buff.delete(0, buff.length());

        newCity.setName("anotherOne");
        assertEquals("Sub-nested notification did not arrive correctly", newCity.getName(), buff.toString());
        buff.delete(0, buff.length());

        currentCity.setName("noNotifExpected");
        assertEquals("Sub-nested notification arrived whereas is shouldn't", "", buff.toString());
        buff.delete(0, buff.length());

        City anotherNewCity = hbc.getEntityFactory().createEntityInstance(City.class);
        anotherNewCity.setName("anotherNewCity");
        anotherNewCity.setZip("12345");

        Employee newManager = hbc.getEntityFactory().createEntityInstance(Employee.class);
        newManager.getContact().setCity(anotherNewCity);
        newManager.setCompany(d.getCompany());
        d.setManager(newManager);
        assertEquals("Sub-nested notification did not arrive correctly", anotherNewCity.getName(), buff.toString());
        buff.delete(0, buff.length());

        anotherNewCity.setName("anotherNewNotif");
        assertEquals("Sub-nested notification did not arrive correctly", anotherNewCity.getName(), buff.toString());
        buff.delete(0, buff.length());
    }

    /**
     * Tests that property cache gets correctly reset when there is no listener
     * (bug #852) and that the property change events are correctly fired when the
     * computed property is supposed to change (bug #1025).
     */
    @Test
    public void testComputedPropertyCacheResetAndNotif() {
        Employee emp = getBackendController().getEntityFactory().createEntityInstance(Employee.class);
        Calendar c = Calendar.getInstance();
        c.set(Calendar.YEAR, 1973);

        emp.setBirthDate(c.getTime());
        assertEquals("Age is not correctly computed.", emp.computeAge(emp.getBirthDate()), emp.getAge());

        c.add(Calendar.YEAR, -3);
        emp.setBirthDate(c.getTime());
        assertEquals("Age is not correctly computed after birth date modification.",
                emp.computeAge(emp.getBirthDate()), emp.getAge());

        final StringBuilder buff = new StringBuilder();
        emp.addPropertyChangeListener(Employee.AGE, new PropertyChangeListener() {

            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                buff.append(evt.getNewValue());
            }
        });
        c.add(Calendar.YEAR, -5);
        emp.setBirthDate(c.getTime());
        assertEquals("Age is not correctly computedafter 2nd birth date modification.",
                emp.computeAge(emp.getBirthDate()), emp.getAge());
        assertTrue("Age notification failed.", buff.length() > 0);
        assertEquals("Age notification contains bad value.", emp.computeAge(emp.getBirthDate()).toString(),
                buff.toString());
    }

    /**
     * Tests Hibernate null components handling.
     * See bug #1041
     */
    @Test
    public void testNullComponentHandling() {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();
        final Company companyWithNullContact = hbc.getEntityFactory().createEntityInstance(Company.class);
        companyWithNullContact.setName("CompanyWithNullContact");
        hbc.getTransactionTemplate().execute(new TransactionCallbackWithoutResult() {

            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                Company cClone = hbc.cloneInUnitOfWork(companyWithNullContact);
                hbc.registerForUpdate(cClone);
            }
        });

        EnhancedDetachedCriteria companyCrit = EnhancedDetachedCriteria.forClass(Company.class);
        List<Company> companies = hbc.findByCriteria(companyCrit, EMergeMode.MERGE_KEEP, Company.class);
        for (Company c : companies) {
            assertNotNull("Company has a null contact (embedded component) instead of an empty one.",
                    c.getContact());
            assertFalse("Company is dirty whereas it shouldn't", hbc.isDirty(c));
        }
    }

    /**
     * Test component dirty properties. See bug #1038
     */
    @Test
    public void testComponentDirtyProperties() {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();
        EnhancedDetachedCriteria crit = EnhancedDetachedCriteria.forClass(Company.class);
        Company c = hbc.findFirstByCriteria(crit, EMergeMode.MERGE_CLEAN_EAGER, Company.class);
        String oldAddress = c.getContact().getAddress();
        c.getContact().setAddress("dirty");
        Map<String, Object> dirtyProperties = hbc.getDirtyProperties(c);
        assertNotNull("Contact old value is null", dirtyProperties.get(Company.CONTACT));
        assertEquals("Old contact address does not have the correct oldValue", oldAddress,
                ((ContactInfo) dirtyProperties.get(Company.CONTACT)).getAddress());
    }

    /**
     * Test component toString. See bug #1042
     */
    @Test
    public void testComponentToString() {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();
        EnhancedDetachedCriteria crit = EnhancedDetachedCriteria.forClass(Company.class);
        Company c = hbc.findFirstByCriteria(crit, EMergeMode.MERGE_CLEAN_EAGER, Company.class);
        ContactInfo companyContact = c.getContact();
        assertEquals("contact toString() should be the holding entity one when not null.", c.toString(),
                companyContact.toString());
        ContactInfo orphanContact = hbc.getEntityFactory().createComponentInstance(ContactInfo.class);
        orphanContact.setAddress("toString test");
        assertEquals("contact toString() should be address when the holding entity is null.", "toString test",
                orphanContact.toString());
    }

    /**
     * Test Collection null elements. See bug #643
     */
    @Test(expected = MandatoryPropertyException.class)
    public void testNullElementAllowed() {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();
        EnhancedDetachedCriteria crit = EnhancedDetachedCriteria.forClass(Employee.class);
        Employee e = hbc.findFirstByCriteria(crit, EMergeMode.MERGE_CLEAN_EAGER, Employee.class);
        e.addToEvents(null);
        hbc.registerForUpdate(e);
        hbc.getTransactionTemplate().execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                hbc.performPendingOperations();
            }
        });
    }

    /**
     * Test Enumeration value rejection. See bug #841
     */
    @Test(expected = IntegrityException.class)
    public void testEnumValueRejection() {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();
        Employee emp = hbc.getEntityFactory().createEntityInstance(Employee.class);
        emp.setGender("C");
    }

    /**
     * Persistent collection substitution on save. See bug #1114
     */
    @Test
    public void testPersistentCollectionSubstitution() {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();
        Company comp = hbc.getEntityFactory().createEntityInstance(Company.class);
        comp.setName("testC");

        Department dept = hbc.getEntityFactory().createEntityInstance(Department.class);
        dept.setName("testD");
        dept.setOuId("TE-001");

        comp.addToDepartments(dept);

        hbc.registerForUpdate(comp);
        hbc.getTransactionTemplate().execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                hbc.performPendingOperations();
            }
        });

        assertTrue("Persistent entity collection is not instance of PersistentCollection",
                comp.straightGetProperty(Company.DEPARTMENTS) instanceof PersistentCollection);
        assertTrue("Entity is transient after save", comp.isPersistent());
    }

    /**
     * Test translated name.
     */
    @Test
    public void testPropertyTranslation() {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();
        Serializable cityId = hbc.getTransactionTemplate().execute(new TransactionCallback<Serializable>() {
            @Override
            public Serializable doInTransaction(TransactionStatus status) {
                City c = hbc.getEntityFactory().createEntityInstance(City.class);
                c.setNameRaw("raw");
                c.setName("test");
                hbc.registerForUpdate(c);
                return c.getId();
            }
        });
        final City c = hbc.findById(cityId, EMergeMode.MERGE_KEEP, City.class);
        assertEquals("A message translation should have been created", 1, c.getPropertyTranslations().size());
        hbc.getTransactionTemplate().execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                City cClone = hbc.cloneInUnitOfWork(c);
                cClone.setName("modified");
            }
        });
        hbc.reload(c);
        assertEquals("The message translation should have been updated", "modified", c.getName());
    }

    /**
     * Tests fix for bug #1115.
     */
    @Test
    public void testRecursiveComponentHashCodeEquals() {
        Link head = getBackendController().getEntityFactory().createComponentInstance(Link.class);
        Link tail = getBackendController().getEntityFactory().createComponentInstance(Link.class);
        head.setName("head");
        tail.setName("tail");
        head.addToChildren(tail);
        tail.setParent(head);
        int hc = head.hashCode();
        assertTrue("The hash code is not correctly computed", hc != 0);
        assertNotEquals("Equality is not correctly computed", head, tail);

        Link left = getBackendController().getEntityFactory().createComponentInstance(Link.class);
        Link right = getBackendController().getEntityFactory().createComponentInstance(Link.class);

        assertEquals("Equality is not correctly computed", left, right);

        left.addToChildren(right);
        right.addToChildren(left);
        left.setParent(right);
        right.setParent(left);

        assertNotEquals("Equality is not correctly computed", left, right);

        left.setName("left");
        right.setName("right");

        assertNotEquals("Equality is not correctly computed", left, right);
    }

    /**
     * Tests fix for bug #1126.
     */
    @Test
    public void testResetUninitializedReference() {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();
        EnhancedDetachedCriteria crit = EnhancedDetachedCriteria.forClass(Employee.class);
        final Employee e = hbc.findFirstByCriteria(crit, EMergeMode.MERGE_KEEP, Employee.class);
        assertFalse(Hibernate.isInitialized(e.straightGetProperty(Employee.COMPANY)));
        e.straightSetProperty(Employee.COMPANY, null);
        e.getCompany();
    }

    /**
     * Test one to one dB access.
     */
    @Test
    public void testOneToOneDBAccess() {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();
        hbc.getTransactionTemplate().execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                EnhancedDetachedCriteria critDept = EnhancedDetachedCriteria.forClass(Department.class);
                critDept.add(Restrictions.like(Department.OU_ID, "HR", MatchMode.START));
                Department dept = hbc.findFirstByCriteria(critDept, EMergeMode.MERGE_KEEP, Department.class);
                assertTrue("OneToOne has not been initialized when querying",
                        Hibernate.isInitialized(dept.straightGetProperty(Department.MANAGER)));
            }
        });
        hbc.getTransactionTemplate().execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                EnhancedDetachedCriteria critEmp = EnhancedDetachedCriteria.forClass(Employee.class);
                critEmp.add(Restrictions.like(Employee.FIRST_NAME, "Gloria", MatchMode.START));
                Employee emp = hbc.findFirstByCriteria(critEmp, EMergeMode.MERGE_KEEP, Employee.class);
                assertFalse("ManyToOne has not been initialized when querying",
                        Hibernate.isInitialized(emp.straightGetProperty(Employee.MANAGED_OU)));
            }
        });
    }

    /**
     * Test lifecycle called at right time. Fixes bug #55
     */
    @Test
    public void testLifecycleCalledAtRightTime() {
        final HibernateBackendController hbc = (HibernateBackendController) getBackendController();

        final Serializable id1 = hbc.getTransactionTemplate().execute(new TransactionCallback<Serializable>() {
            @Override
            public Serializable doInTransaction(TransactionStatus status) {
                Company company = hbc.getEntityFactory().createEntityInstance(Company.class);
                company.setName("testComp");
                hbc.registerForUpdate(company);
                // Old behaviour restored.
                //assertNull("Create timestamp assigned too early", company.getCreateTimestamp());
                company.setName("testCompMod");
                return company.getId();
            }
        });
        Company sessionCompany = hbc.findById(id1, EMergeMode.MERGE_KEEP, Company.class);
        assertNotNull("Create timestamp not assigned", sessionCompany.getCreateTimestamp());
        assertNull("Last update timestamp assigned, but should not be", sessionCompany.getLastUpdateTimestamp());

        final Serializable id2 = hbc.getTransactionTemplate().execute(new TransactionCallback<Serializable>() {
            @Override
            public Serializable doInTransaction(TransactionStatus status) {
                Company company = hbc.getEntityFactory().createEntityInstance(Company.class);
                company.setName("testComp2");
                hbc.registerForUpdate(company);
                hbc.flush();
                assertNotNull("Create timestamp not assigned after flush", company.getCreateTimestamp());
                company.setName("testComp2Mod");
                return company.getId();
            }
        });
        Company sessionCompany2 = hbc.findById(id2, EMergeMode.MERGE_KEEP, Company.class);
        assertNotNull("Create timestamp not assigned", sessionCompany2.getCreateTimestamp());
        assertNotNull("Last update timestamp not assigned", sessionCompany2.getLastUpdateTimestamp());

        hbc.getTransactionTemplate().execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                Company company1 = hbc.findById(id1, EMergeMode.MERGE_KEEP, Company.class);
                Company company2 = hbc.findById(id2, EMergeMode.MERGE_KEEP, Company.class);
                hbc.registerForDeletion(company1);
                hbc.registerForDeletion(company2);
            }
        });
        assertNull("Company 1 not correctly deleted.", hbc.findById(id1, EMergeMode.MERGE_KEEP, Company.class));
        assertNull("Company 2 not correctly deleted.", hbc.findById(id2, EMergeMode.MERGE_KEEP, Company.class));
    }

    static class PropertyMatcher extends ArgumentMatcher<Object[]> {

        private final String propertyName;

        /**
         * Constructs a new {@code PropertyMatcher} instance.
         *
         * @param propertyName the property name
         */
        public PropertyMatcher(String propertyName) {
            this.propertyName = propertyName;
        }

        @Override
        public boolean matches(Object param) {
            Object[] args = (Object[]) param;
            return args != null && args.length > 0 && propertyName.equals(args[0]);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void describeTo(Description description) {
            super.describeTo(description);
            description.appendText("[" + propertyName + "]");
        }
    }
}