/* * Copyright (c) 2009, SQL Power Group Inc. * * This file is part of SQL Power Library. * * SQL Power Library is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * SQL Power Library 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package ca.sqlpower.object; import java.awt.Image; import java.beans.PropertyDescriptor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.beanutils.BeanUtils; import org.apache.commons.beanutils.PropertyUtils; import org.apache.log4j.Logger; import ca.sqlpower.dao.PersistedSPOProperty; import ca.sqlpower.dao.PersistedSPObject; import ca.sqlpower.dao.PersisterUtils; import ca.sqlpower.dao.SPPersister.DataType; import ca.sqlpower.dao.SPPersisterListener; import ca.sqlpower.dao.SPSessionPersister; import ca.sqlpower.dao.session.SessionPersisterSuperConverter; import ca.sqlpower.object.SPChildEvent.EventType; import ca.sqlpower.object.annotation.Accessor; import ca.sqlpower.object.annotation.Mutator; import ca.sqlpower.object.annotation.NonProperty; import ca.sqlpower.sql.DataSourceCollection; import ca.sqlpower.sql.SPDataSource; import ca.sqlpower.sqlobject.DatabaseConnectedTestCase; import ca.sqlpower.sqlobject.SQLObjectRoot; import ca.sqlpower.testutil.GenericNewValueMaker; import ca.sqlpower.testutil.NewValueMaker; import ca.sqlpower.testutil.SPObjectRoot; import ca.sqlpower.testutil.TestUtils; import ca.sqlpower.util.RunnableDispatcher; import ca.sqlpower.util.StubWorkspaceContainer; import ca.sqlpower.util.TransactionEvent; import ca.sqlpower.util.WorkspaceContainer; import com.google.common.collect.ImmutableList; /** * Classes that implement SPObject and need to be persisted must implement * a test class that extends this test case. */ public abstract class PersistedSPObjectTest extends DatabaseConnectedTestCase { private static final Logger logger = Logger.getLogger(PersistedSPObjectTest.class); public class TestingSessionPersister extends SPSessionPersister { public TestingSessionPersister(String name, SPObject root, SessionPersisterSuperConverter converter) { super(name, root, converter); } @Override protected void refreshRootNode(PersistedSPObject pso) { //do nothing, this is not tested in a generic way. } } /** * Returns a class that is one of the child types of the object under test. An * object of this type must be able to be added as a child to the object without * error. If the object under test does not allow children or all of the children * of the object are final so none can be added, null will be returned. */ protected abstract Class<? extends SPObject> getChildClassType(); /** * This workspace contains the root SPObject made in setup. This is only needed * for connecting the root to a session in setup. If a formal root object * for a session gets created in the library it can replace this stub version. */ public static class StubWorkspace extends AbstractSPObject { public static final List<Class<? extends SPObject>> allowedChildTypes = new ImmutableList.Builder<Class<? extends SPObject>>() .add(SPObject.class) .build(); private final WorkspaceContainer workspaceContainer; private final RunnableDispatcher dispatcher; private final SPObjectRoot root; public StubWorkspace(WorkspaceContainer workspaceContainer, RunnableDispatcher dispatcher, SPObjectRoot root) { this.workspaceContainer = workspaceContainer; this.dispatcher = dispatcher; this.root = root; } @Override protected boolean removeChildImpl(SPObject child) { return false; } public List<Class<? extends SPObject>> getAllowedChildTypes() { return allowedChildTypes; } public List<? extends SPObject> getChildren() { return Collections.singletonList(root); } public List<? extends SPObject> getDependencies() { return Collections.emptyList(); } public void removeDependency(SPObject dependency) { //do nothing } @Override public WorkspaceContainer getWorkspaceContainer() { return workspaceContainer; } @Override public RunnableDispatcher getRunnableDispatcher() { return dispatcher; } } /** * Used in roll back tests. If an exception is thrown due to failing a test * the rollback will catch the exception and try to roll back the object. * However, we want to know if the persist failed before rolling back. This * object will store the failed reason during persist. */ private Throwable failureReason; private SPObjectRoot root; /** * This is a generic converter that works off the root object and pl.ini in * this test. This will need to be set to a different converter in tests * that are outside of the library. */ private SessionPersisterSuperConverter converter; public PersistedSPObjectTest(String name) { super(name); } public PersistedSPObjectTest(String name, boolean setupDB) { super(name, setupDB); } @Override protected void setUp() throws Exception { super.setUp(); root = new SPObjectRoot(); StubWorkspaceContainer stub = new StubWorkspaceContainer() { private final SPObject workspace = new StubWorkspace(this, this, root); @Override public SPObject getWorkspace() { return workspace; } }; root.setParent(stub.getWorkspace()); SQLObjectRoot sqlRoot = new SQLObjectRoot(); root.addChild(sqlRoot, 0); if (setupDB) { sqlRoot.addDatabase(db, 0); } converter = new SessionPersisterSuperConverter( getPLIni(), root); } /** * Returns an object of the type being tested. Will be used in reflective * tests being done for persisting objects. This must be a descendant of the * root object returned from {@link #getRootObject()}. */ public abstract SPObject getSPObjectUnderTest(); /** * Returns the converter to be used in persister tests. This can be * overridden by other test classes to specify a converter that is different * from the basic one in the library. */ public SessionPersisterSuperConverter getConverter() { return converter; } /** * Returns a new new-value-maker that will attach {@link SPObject}s to the given root. * Classes extending this test may want to override this method to create a different * type of new-value-maker that creates more types of new values. */ public NewValueMaker createNewValueMaker(SPObject root, DataSourceCollection<SPDataSource> dsCollection) { return new GenericNewValueMaker(root, dsCollection); } public SPObject getRootObject() { return root; } /** * This function is here to be overidden in special cases. eg = MungeProcess. * Since there is a final child of ResultStep and the child type, you need to * offset the index you add the child add by 1. */ public int getIndexToInsertChildAt() { return 0; } /** * All persistable {@link SPObject} implementations must define a static * final field which is a list defining the absolute ordering of that * class's child type classes. This method ensures that list is retrievable * by reflection from the object, that the field is public, static, and * final, and that it is nonempty for classes that allow children and empty * for classes that do not allow children. */ @SuppressWarnings("unchecked") public void testAllowedChildTypesField() throws Exception { Class<? extends SPObject> classUnderTest = getSPObjectUnderTest().getClass(); Field childOrderField; try { childOrderField = classUnderTest.getDeclaredField("allowedChildTypes"); } catch (NoSuchFieldException ex) { fail("Persistent " + classUnderTest + " must have a static final field called allowedChildTypes"); throw new AssertionError(); // NOTREACHED } assertEquals("The allowedChildTypes field must be final", true, Modifier.isFinal(childOrderField.getModifiers())); assertEquals("The allowedChildTypes field must be static", true, Modifier.isStatic(childOrderField.getModifiers())); // Note: in the future, we will change this to require that the field is private assertEquals("The allowedChildTypes field must be public", true, Modifier.isPublic(childOrderField.getModifiers())); List<Class<? extends SPObject>> allowedChildTypes = (List<Class<? extends SPObject>>) childOrderField.get(null); if (getSPObjectUnderTest().allowsChildren()) { assertFalse(allowedChildTypes.isEmpty()); } else { assertTrue(allowedChildTypes.isEmpty()); } } /** * Tests the SPPersisterListener will persist a property change to its * target persister. */ public void testSPListenerPersistsProperty() throws Exception { CountingSPPersister countingPersister = new CountingSPPersister(); SPPersisterListener listener = new SPPersisterListener(countingPersister, getConverter()); NewValueMaker valueMaker = createNewValueMaker(root, getPLIni()); SPObject wo = getSPObjectUnderTest(); wo.addSPListener(listener); List<PropertyDescriptor> settableProperties; settableProperties = Arrays.asList(PropertyUtils.getPropertyDescriptors(wo.getClass())); Set<String> propertiesToPersist = findPersistableBeanProperties(false, false); for (PropertyDescriptor property : settableProperties) { Object oldVal; if (!propertiesToPersist.contains(property.getName())) continue; countingPersister.clearAllPropertyChanges(); try { oldVal = PropertyUtils.getSimpleProperty(wo, property.getName()); // check for a setter if (property.getWriteMethod() == null) continue; } catch (NoSuchMethodException e) { logger.debug("Skipping non-settable property " + property.getName() + " on " + wo.getClass().getName()); continue; } Object newVal = valueMaker.makeNewValue(property.getPropertyType(), oldVal, property.getName()); int oldChangeCount = countingPersister.getPersistPropertyCount(); try { //The first property change at current is always the property change we are //looking for, this may need to be changed in the future to find the correct //property. PersistedSPOProperty propertyChange = null; try { logger.debug("Setting property '" + property.getName() + "' to '" + newVal + "' (" + newVal.getClass().getName() + ")"); wo.setMagicEnabled(false); BeanUtils.copyProperty(wo, property.getName(), newVal); assertTrue("Did not persist property " + property.getName(), oldChangeCount < countingPersister.getPersistPropertyCount()); for (PersistedSPOProperty nextPropertyChange : countingPersister.getPersistPropertyList()) { if (nextPropertyChange.getPropertyName().equals(property.getName())) { propertyChange = nextPropertyChange; break; } } assertNotNull("A property change event cannot be found for the property " + property.getName(), propertyChange); assertEquals(wo.getUUID(), propertyChange.getUUID()); assertEquals(property.getName(), propertyChange.getPropertyName()); assertEquals("Old value of property " + property.getName() + " was wrong, value expected was " + oldVal + " but is " + countingPersister.getLastOldValue(), getConverter().convertToBasicType(oldVal), propertyChange.getOldValue()); } finally { wo.setMagicEnabled(true); } //Input streams from images are being compared by hash code not values if (Image.class.isAssignableFrom(property.getPropertyType())) { logger.debug(propertyChange.getNewValue().getClass()); assertTrue(Arrays.equals(PersisterUtils.convertImageToStreamAsPNG( (Image) newVal).toByteArray(), PersisterUtils.convertImageToStreamAsPNG( (Image) getConverter().convertToComplexType( propertyChange.getNewValue(), Image.class)).toByteArray())); } else { assertEquals(getConverter().convertToBasicType(newVal), propertyChange.getNewValue()); } Class<? extends Object> classType; if (oldVal != null) { classType = oldVal.getClass(); } else { classType = newVal.getClass(); } assertEquals(PersisterUtils.getDataType(classType), propertyChange.getDataType()); } catch (InvocationTargetException e) { logger.debug("(non-fatal) Failed to write property '"+property.getName()+" to type "+wo.getClass().getName()); } } } /** * Tests the {@link SPSessionPersister} can update every settable property * on an object based on a persist call. */ public void testSPPersisterPersistsProperties() throws Exception { SPSessionPersister persister = new TestingSessionPersister( "Testing Persister", root, getConverter()); persister.setWorkspaceContainer(root.getWorkspaceContainer()); NewValueMaker valueMaker = createNewValueMaker(root, getPLIni()); SPObject objectUnderTest = getSPObjectUnderTest(); List<PropertyDescriptor> settableProperties = Arrays.asList( PropertyUtils.getPropertyDescriptors(objectUnderTest.getClass())); Set<String> propertiesToPersist = findPersistableBeanProperties(false, false); for (PropertyDescriptor property : settableProperties) { Object oldVal; //Changing the UUID of the object makes it referenced as a different object //and would make the check later in this test fail. if (property.getName().equals("UUID")) continue; if (!propertiesToPersist.contains(property.getName())) continue; try { oldVal = PropertyUtils.getSimpleProperty(objectUnderTest, property.getName()); // check for a setter if (property.getWriteMethod() == null) continue; } catch (NoSuchMethodException e) { logger.debug("Skipping non-settable property " + property.getName() + " on " + objectUnderTest.getClass().getName()); continue; } //special case for parent types. If a specific wabit object has a tighter parent then //WabitObject the getParentClass should return the parent type. Class<?> propertyType = property.getPropertyType(); if (property.getName().equals("parent")) { propertyType = getSPObjectUnderTest().getClass().getMethod("getParent").getReturnType(); logger.debug("Persisting parent, type is " + propertyType); } Object newVal = valueMaker.makeNewValue(propertyType, oldVal, property.getName()); System.out.println("Persisting property \"" + property.getName() + "\" from oldVal \"" + oldVal + "\" to newVal \"" + newVal + "\""); DataType type = PersisterUtils.getDataType(property.getPropertyType()); Object basicNewValue = getConverter().convertToBasicType(newVal); persister.begin(); persister.persistProperty(objectUnderTest.getUUID(), property.getName(), type, getConverter().convertToBasicType(oldVal), basicNewValue); persister.commit(); Object newValAfterSet = PropertyUtils.getSimpleProperty(objectUnderTest, property.getName()); Object basicExpectedValue = getConverter().convertToBasicType(newValAfterSet); assertPersistedValuesAreEqual(newVal, newValAfterSet, basicNewValue, basicExpectedValue, property.getPropertyType()); } } /** * This test will be run for each object that extends SPObject and confirms * the SPSessionPersister can create new objects * @throws Exception */ public void testPersisterCreatesNewObjects() throws Exception { SPObjectRoot newRoot = new SPObjectRoot(); WorkspaceContainer stub = new StubWorkspaceContainer() { private final SPObject workspace = new StubWorkspace(this, this, root); @Override public SPObject getWorkspace() { return workspace; } }; newRoot.setParent(stub.getWorkspace()); NewValueMaker valueMaker = createNewValueMaker(root, getPLIni()); NewValueMaker newValueMaker = createNewValueMaker(newRoot, getPLIni()); SessionPersisterSuperConverter newConverter = new SessionPersisterSuperConverter( getPLIni(), newRoot); SPSessionPersister persister = new TestingSessionPersister("Test persister", newRoot, newConverter); persister.setWorkspaceContainer(stub); for (SPObject child : root.getChildren()) { copyToRoot(child, newValueMaker); } SPObject objectUnderTest = getSPObjectUnderTest(); Set<String> propertiesToPersist = findPersistableBeanProperties(false, false); List<PropertyDescriptor> settableProperties = Arrays.asList( PropertyUtils.getPropertyDescriptors(objectUnderTest.getClass())); //set all properties of the object for (PropertyDescriptor property : settableProperties) { Object oldVal; if (!propertiesToPersist.contains(property.getName())) continue; if (property.getName().equals("parent")) continue; //Changing the parent causes headaches. try { oldVal = PropertyUtils.getSimpleProperty(objectUnderTest, property.getName()); // check for a setter if (property.getWriteMethod() == null) continue; } catch (NoSuchMethodException e) { logger.debug("Skipping non-settable property " + property.getName() + " on " + objectUnderTest.getClass().getName()); continue; } Object newVal = valueMaker.makeNewValue(property.getPropertyType(), oldVal, property.getName()); Object newValInNewRoot = newValueMaker.makeNewValue(property.getPropertyType(), oldVal, property.getName()); if (newValInNewRoot instanceof SPObject) { ((SPObject) newValInNewRoot).setUUID(((SPObject) newVal).getUUID()); } try { logger.debug("Setting property '" + property.getName() + "' to '" + newVal + "' (" + newVal.getClass().getName() + ")"); BeanUtils.copyProperty(objectUnderTest, property.getName(), newVal); } catch (InvocationTargetException e) { logger.debug("(non-fatal) Failed to write property '" + property.getName() + " to type " + objectUnderTest.getClass().getName()); } } //create a new root and parent for the object SPObject newParent; if (objectUnderTest.getParent() instanceof SPObjectRoot) { newParent = newRoot; } else { newParent = (SPObject) newValueMaker.makeNewValue( objectUnderTest.getParent().getClass(), null, ""); } newParent.setUUID(objectUnderTest.getParent().getUUID()); int childCount = newParent.getChildren().size(); //persist the object to the new target root Class<? extends SPObject> classChildType = PersisterUtils.getParentAllowedChildType( objectUnderTest.getClass().getName(), objectUnderTest.getParent().getClass().getName()); new SPPersisterListener(persister, getConverter()).persistObject(objectUnderTest, objectUnderTest.getParent().getChildren(classChildType).indexOf(objectUnderTest)); //check object exists assertEquals(childCount + 1, newParent.getChildren().size()); SPObject newChild = null; for (SPObject child : newParent.getChildren()) { if (child.getUUID().equals(objectUnderTest.getUUID())) { newChild = child; break; } } if (newChild == null) fail("The child was not correctly persisted."); //check all interesting properties for (PropertyDescriptor property : settableProperties) { if (!propertiesToPersist.contains(property.getName())) continue; if (property.getName().equals("parent")) continue; //Changing the parent causes headaches. Method readMethod = property.getReadMethod(); Object valueBeforePersist = readMethod.invoke(objectUnderTest); Object valueAfterPersist = readMethod.invoke(newChild); Object basicValueBeforePersist = getConverter().convertToBasicType(valueBeforePersist); Object basicValueAfterPersist = newConverter.convertToBasicType(valueAfterPersist); assertPersistedValuesAreEqual(valueBeforePersist, valueAfterPersist, basicValueBeforePersist, basicValueAfterPersist, readMethod.getReturnType()); } } /** * Helper method for making one object tree contain the same values of the * other tree. The objects created in the new root are not guaranteed to * have the same hierarchy as the original parent-child ordering but is fine * for current testing. * * @param child * The child object that a new object of the same type will be * created and added to the new root. All of its descendants will * be added to the new root as well. * @param newValueMaker * A {@link NewValueMaker} containing the root of the new object * tree that can have new children added to it. */ private void copyToRoot(SPObject child, NewValueMaker newValueMaker) { if (child != getSPObjectUnderTest()) { if (getSPObjectUnderTest().getParent() != null && child == getSPObjectUnderTest().getParent()) return; SPObject newValue = (SPObject) newValueMaker.makeNewValue(child.getClass(), child, "Duplicated child"); newValue.setUUID(child.getUUID()); for (SPObject descendant : child.getChildren()) { copyToRoot(descendant, newValueMaker); } } } /** * Tests passing an object to an {@link SPPersisterListener} will persist * the object and all of the properties that have setters. */ public void testSPListenerPersistsNewObjects() throws Exception { CountingSPPersister persister = new CountingSPPersister(); NewValueMaker valueMaker = createNewValueMaker(root, getPLIni()); SPObject objectUnderTest = getSPObjectUnderTest(); Set<String> propertiesToPersist = findPersistableBeanProperties(false, false); List<PropertyDescriptor> settableProperties = Arrays.asList( PropertyUtils.getPropertyDescriptors(objectUnderTest.getClass())); //set all properties of the object for (PropertyDescriptor property : settableProperties) { Object oldVal; if (!propertiesToPersist.contains(property.getName())) continue; if (property.getName().equals("parent")) continue; //Changing the parent causes headaches. try { oldVal = PropertyUtils.getSimpleProperty(objectUnderTest, property.getName()); // check for a setter if (property.getWriteMethod() == null) continue; } catch (NoSuchMethodException e) { logger.debug("Skipping non-settable property " + property.getName() + " on " + objectUnderTest.getClass().getName()); continue; } Object newVal = valueMaker.makeNewValue(property.getPropertyType(), oldVal, property.getName()); try { logger.debug("Setting property '" + property.getName() + "' to '" + newVal + "' (" + newVal.getClass().getName() + ")"); BeanUtils.copyProperty(objectUnderTest, property.getName(), newVal); } catch (InvocationTargetException e) { logger.debug("(non-fatal) Failed to write property '" + property.getName() + " to type " + objectUnderTest.getClass().getName()); } } //persist the object to the new target root new SPPersisterListener(persister, getConverter()).persistObject(objectUnderTest, objectUnderTest.getParent().getChildren(objectUnderTest.getClass()).indexOf(objectUnderTest)); assertTrue(persister.getPersistPropertyCount() > 0); assertEquals(getSPObjectUnderTest().getUUID(), persister.getPersistObjectList().get(0).getUUID()); //set all properties of the object for (PropertyDescriptor property : settableProperties) { Object oldVal; if (!propertiesToPersist.contains(property.getName())) continue; if (property.getName().equals("parent")) continue; //Changing the parent causes headaches. try { oldVal = PropertyUtils.getSimpleProperty(objectUnderTest, property.getName()); // check for a setter if (property.getWriteMethod() == null) continue; } catch (NoSuchMethodException e) { logger.debug("Skipping non-settable property " + property.getName() + " on " + objectUnderTest.getClass().getName()); continue; } Object newValue = null; boolean found = false; for (PersistedSPOProperty persistedSPO : persister.getPersistPropertyList()) { if (persistedSPO.getPropertyName().equals(property.getName()) && persistedSPO.getUUID().equals(getSPObjectUnderTest().getUUID())) { newValue = persistedSPO.getNewValue(); found = true; break; } } assertTrue("Could not find the persist call for property " + property.getName(), found); if (oldVal == null) { assertNull(newValue); } else { assertPersistedValuesAreEqual(oldVal, getConverter().convertToComplexType(newValue, oldVal.getClass()), getConverter().convertToBasicType(oldVal), newValue, property.getPropertyType()); } } } /** * This method can be overridden by extending classes to specify properties * that should not be written to by the roll back test. The properties put * in this list should have a good reason for not being used in the test as * skipping a property defeats the purpose of the test. */ public Set<String> getRollbackTestIgnorePropertySet() { return new HashSet<String>(); } /** * This test will make changes to the {@link SPObject} under test and then * cause an exception forcing the persister to roll back the changes in the * object. * <p> * Both the changes have to come through the persister initially before the * exception and they have to be reset after the exception. */ public void testSessionPersisterRollsBackProperties() throws Exception { SPObject objectUnderTest = getSPObjectUnderTest(); final Map<PropertyDescriptor, Object> initialProperties = new HashMap<PropertyDescriptor, Object>(); final Map<PropertyDescriptor, Object> newProperties = new HashMap<PropertyDescriptor, Object>(); List<PropertyDescriptor> settableProperties = Arrays.asList( PropertyUtils.getPropertyDescriptors(objectUnderTest.getClass())); Set<String> propertiesToPersist = findPersistableBeanProperties(false, false); Set<String> ignorePropertySet = getRollbackTestIgnorePropertySet(); NewValueMaker valueMaker = createNewValueMaker(getRootObject(), getPLIni()); SPSessionPersister persister = new TestingSessionPersister("tester", getRootObject(), getConverter()); persister.setWorkspaceContainer(getRootObject().getWorkspaceContainer()); failureReason = null; SPPersisterListener listener = new SPPersisterListener(new CountingSPPersister(), converter) { private boolean transactionAlreadyFinished = false; @Override public void transactionEnded(TransactionEvent e) { if (transactionAlreadyFinished) return; transactionAlreadyFinished = true; try { for (Map.Entry<PropertyDescriptor, Object> newProperty : newProperties.entrySet()) { Object objectUnderTest = getSPObjectUnderTest(); Object newVal = newProperty.getValue(); Object basicNewValue = converter.convertToBasicType(newVal); Object newValAfterSet = PropertyUtils.getSimpleProperty( objectUnderTest, newProperty.getKey().getName()); Object basicExpectedValue = converter.convertToBasicType(newValAfterSet); logger.debug("Testing property " + newProperty.getKey().getName()); assertPersistedValuesAreEqual(newVal, newValAfterSet, basicNewValue, basicExpectedValue, newProperty.getKey().getPropertyType()); } } catch (Throwable ex) { failureReason = ex; throw new RuntimeException(ex); } throw new RuntimeException("Forcing rollback."); } }; //Transactions begin and commits are currently sent on the workspace. getRootObject().getParent().addSPListener(listener); persister.begin(); for (PropertyDescriptor property : settableProperties) { Object oldVal; //Changing the UUID of the object makes it referenced as a different object //and would make the check later in this test fail. if (property.getName().equals("UUID")) continue; if (!propertiesToPersist.contains(property.getName())) continue; if (ignorePropertySet.contains(property.getName())) continue; try { oldVal = PropertyUtils.getSimpleProperty(objectUnderTest, property.getName()); // check for a setter if (property.getWriteMethod() == null) continue; } catch (NoSuchMethodException e) { logger.debug("Skipping non-settable property " + property.getName() + " on " + objectUnderTest.getClass().getName()); continue; } initialProperties.put(property, oldVal); //special case for parent types. If a specific wabit object has a tighter parent then //WabitObject the getParentClass should return the parent type. Class<?> propertyType = property.getPropertyType(); if (property.getName().equals("parent")) { propertyType = getSPObjectUnderTest().getClass().getMethod("getParent").getReturnType(); logger.debug("Persisting parent, type is " + propertyType); } Object newVal = valueMaker.makeNewValue(propertyType, oldVal, property.getName()); DataType type = PersisterUtils.getDataType(property.getPropertyType()); Object basicNewValue = converter.convertToBasicType(newVal); persister.begin(); persister.persistProperty(objectUnderTest.getUUID(), property.getName(), type, converter.convertToBasicType(oldVal), basicNewValue); persister.commit(); newProperties.put(property, newVal); } try { persister.commit(); fail("An exception should make the persister hit the exception block."); } catch (Exception e) { //continue, exception expected. } if (failureReason != null) { throw new RuntimeException("Failed when asserting properties were " + "fully persisted.", failureReason); } for (Map.Entry<PropertyDescriptor, Object> entry : initialProperties.entrySet()) { assertEquals("Property " + entry.getKey().getName() + " did not match after rollback.", entry.getValue(), PropertyUtils.getSimpleProperty(objectUnderTest, entry.getKey().getName())); } } /** * Taken from AbstractWabitObjectTest. When Wabit is using annotations * remove this method from that class as this class will be doing the reflective * tests. * <p> * Tests that the new value that was persisted is the same as an old value * that was to be persisted. This helper method for the persister tests will * compare the values by their converted type or some other means as not all * values that are persisted have implemented their equals method. * <p> * This will do the asserts to compare if the objects are equal. * * @param valueBeforePersist * the value that we are expecting the persisted value to contain * @param valueAfterPersist * the value that was persisted to the object. This will be * tested against the valueBeforePersist to ensure that they are * the same. * @param basicValueBeforePersist * The valueBeforePersist converted to a basic type by a * converter. * @param basicValueAfterPersist * The valueAfterPersist converted to a basic type by a * converter. * @param valueType * The type of object the before and after values should contain. */ private void assertPersistedValuesAreEqual(Object valueBeforePersist, Object valueAfterPersist, Object basicValueBeforePersist, Object basicValueAfterPersist, Class<? extends Object> valueType) { //Input streams from images are being compared by hash code not values if (Image.class.isAssignableFrom(valueType)) { assertTrue(Arrays.equals(PersisterUtils.convertImageToStreamAsPNG((Image) valueBeforePersist).toByteArray(), PersisterUtils.convertImageToStreamAsPNG((Image) valueAfterPersist).toByteArray())); } else if (Exception.class.isAssignableFrom(valueType)) { //Comparing only the first part of the exception strings as the new exception created by the persistence class //will have the converter after the original stack trace since that is where the exception was made. assertTrue("Persist failed for type " + valueType, ((String) basicValueAfterPersist).startsWith((String) basicValueBeforePersist)); } else { //Not all new values are equivalent to their old values so we are //comparing them by their basic type as that is at least comparable, in most cases, i hope. assertEquals("Persist failed for type " + valueType, basicValueBeforePersist, basicValueAfterPersist); } } /** * Ensures that each getter and setter in the object under test is annotated * in some way. This way methods that need to be annotated to be persisted * will not be missed or will be defined to be skipped. The annotations are either * {@link Accessor} for getters, {@link Mutator} for setters, and {@link NonProperty} * that is neither an accessor or mutator but looks like one. */ public void testGettersAndSettersPersistedAnnotated() throws Exception { findPersistableBeanProperties(false, false); } protected Set<String> findPersistableBeanProperties(boolean includeTransient, boolean includeConstructorMutators) throws Exception { return TestUtils.findPersistableBeanProperties(getSPObjectUnderTest(), includeTransient, includeConstructorMutators); } /** * Tests a child can be added to the {@link SPObject} under test. If the * object does not allow children then this test will return early. This * test is used as a start to the remove child test. * * @return The child that was added or null if no child was added. * @throws Exception */ public SPObject testSPPersisterAddsChild() throws Exception { NewValueMaker valueMaker = createNewValueMaker(root, getPLIni()); SPObject spObject = getSPObjectUnderTest(); int oldChildCount = spObject.getChildren().size(); if (!spObject.allowsChildren()) return null; Class<? extends SPObject> childClassType = getChildClassType(); if (childClassType == null) return null; SPSessionPersister persister = new TestingSessionPersister("test", getSPObjectUnderTest(), getConverter()); persister.setWorkspaceContainer(getSPObjectUnderTest().getWorkspaceContainer()); SPPersisterListener listener = new SPPersisterListener(persister, getConverter()); SPObject newChild = (SPObject) valueMaker.makeNewValue(childClassType, null, "child"); newChild.setParent(spObject); listener.childAdded(new SPChildEvent(spObject, childClassType, newChild, getIndexToInsertChildAt(), EventType.ADDED)); assertEquals(oldChildCount + 1, spObject.getChildren().size()); assertEquals(newChild, spObject.getChildren(childClassType).get(getIndexToInsertChildAt())); newChild.removeSPListener(listener); //Find the actual child under the object under test as the persister will make a new, //different object to add not the newChild object. This lets the objects compare //equal by reference. for (SPObject existingChild : spObject.getChildren(childClassType)) { if (existingChild.getUUID().equals(newChild.getUUID())) { return existingChild; } } return null; } /** * Confirms a child can be removed from an object it was previously added to. * This uses {@link #testSPPersisterAddsChild()} as a starting point. */ public void testSPPersisterRemovesChild() throws Exception { if (!getSPObjectUnderTest().allowsChildren()) return; SPObject child = testSPPersisterAddsChild(); if (child == null) return; SPSessionPersister persister = new TestingSessionPersister("test", getSPObjectUnderTest(), getConverter()); persister.setWorkspaceContainer(getSPObjectUnderTest().getWorkspaceContainer()); SPPersisterListener listener = new SPPersisterListener(persister, getConverter()); int childCount = getSPObjectUnderTest().getChildren().size(); listener.childRemoved(new SPChildEvent(getSPObjectUnderTest(), child.getClass(), child, getIndexToInsertChildAt(), EventType.REMOVED)); assertEquals(childCount - 1, getSPObjectUnderTest().getChildren().size()); assertFalse(getSPObjectUnderTest().getChildren().contains(child)); } /** * Tests that the parent property of a child is only and must set to null * after firing a child removed event. If the parent is set to null before * firing the child removed event, * {@link SPPersisterListener#propertyChanged(java.beans.PropertyChangeEvent)} * will throw an exception because its root object does not have a runnable * dispatcher reference. If the parent is not set to null after firing the * child removed event, anything that still has a reference to the child * could call getParent() and be returned a non-null value which is * incorrect and misleading. */ public void testNullParentSetAfterChildRemovedEvent() throws Exception { if (!getSPObjectUnderTest().allowsChildren()) return; SPObject child = testSPPersisterAddsChild(); if (child == null) return; SPListener listener = new AbstractSPListener() { @Override public void childRemoved(SPChildEvent e) { assertNotNull("Parent of " + e.getChildType() + " must not be set to null before calling removeChild.", e.getChild().getParent()); } }; getSPObjectUnderTest().addSPListener(listener); getSPObjectUnderTest().setMagicEnabled(false); getSPObjectUnderTest().removeChild(child); getSPObjectUnderTest().setMagicEnabled(true); assertNull("Parent of " + child.getClass() + " must be set to null after calling removeChild.", child.getParent()); } public void testRemoveChildFiresEvent() throws Exception { if (!getSPObjectUnderTest().allowsChildren()) return; SPObject child = testSPPersisterAddsChild(); if (child == null) return; CountingSPListener listener = new CountingSPListener(); getSPObjectUnderTest().addSPListener(listener); getSPObjectUnderTest().setMagicEnabled(false); getSPObjectUnderTest().removeChild(child); getSPObjectUnderTest().setMagicEnabled(true); assertEquals(1, listener.getChildRemovedCount()); } public void testAddChildFiresEvents() throws Exception { SPObject o = getSPObjectUnderTest(); if (!o.allowsChildren()) return; Class<?> childClassType = getChildClassType(); if (childClassType == null) return; CountingSPListener listener = new CountingSPListener(); o.addSPListener(listener); NewValueMaker valueMaker = createNewValueMaker(root, getPLIni()); SPObject newChild = (SPObject) valueMaker.makeNewValue(childClassType, null, "child"); o.addChild(newChild, getIndexToInsertChildAt()); assertEquals(1, listener.getChildAddedCount()); } }