Jpa merge support is now available.
authorErik Brakkee <erik@brakkee.org>
Fri, 30 Apr 2010 12:47:01 +0000 (12:47 +0000)
committerErik Brakkee <erik@brakkee.org>
Fri, 30 Apr 2010 12:47:01 +0000 (12:47 +0000)
support/general/src/main/java/org/wamblee/persistence/JpaMergeSupport.java [new file with mode: 0644]
support/general/src/main/java/org/wamblee/persistence/PersistentFactory.java
support/general/src/test/java/org/wamblee/persistence/JpaMergeSupportTest.java [new file with mode: 0644]
support/general/src/test/java/org/wamblee/persistence/PersistentFactoryTest.java

diff --git a/support/general/src/main/java/org/wamblee/persistence/JpaMergeSupport.java b/support/general/src/main/java/org/wamblee/persistence/JpaMergeSupport.java
new file mode 100644 (file)
index 0000000..be188e2
--- /dev/null
@@ -0,0 +1,320 @@
+/*
+ * Copyright 2005-2010 the original author or authors.
+ * 
+ * Licensed under the Apache License, Version 2.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://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.wamblee.persistence;
+
+import java.io.Serializable;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Map.Entry;
+
+import javax.persistence.EntityManager;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.wamblee.persistence.PersistentFactory.EntityAccessor;
+import org.wamblee.reflection.ReflectionUtils;
+
+/**
+ * Support for merging of JPA entities. This utility allows the result of a
+ * merge (modifications of primary key and/or version) to be merged back into
+ * the argument that was merged. As a result, the merged entity can be reused
+ * and the application is not forced to use the new version that was returned
+ * from the merge.
+ * 
+ * The utility traverses the object graph based on the public getter methods.
+ * Therefore, care should be taken with this utility as usage could lead to
+ * recursively loading all objects reachable from the given object. Then again,
+ * this utility is for working with detached objects and it would, in general,
+ * be bad practice to work with detached objects that still contain unresolved
+ * lazy loaded relations and with detached objects that implicitly refer to
+ * almost the entire datamodel.
+ * 
+ * This utility best supports a service oriented design where interaction is
+ * through service interfaces where each service has its own storage isolated
+ * from other services. That would be opposed to a shared data design with many
+ * services acting on the same data.
+ * 
+ * @author Erik Brakkee
+ */
+public class JpaMergeSupport {
+    private static final Log LOG = LogFactory.getLog(JpaMergeSupport.class);
+
+    /**
+     * Constructs the object.
+     * 
+     */
+    public JpaMergeSupport() {
+        // Empty
+    }
+
+    /**
+     * As {@link #merge(Persistent)} but with a given template. This method can
+     * be accessed in a static way.
+     * 
+     * @param aMerge
+     *            The result of the call to {@link EntityManager#merge(Object)}.
+     * @param aPersistent
+     *            Object that was passed to {@link EntityManager#merge(Object)}.
+     */
+    public static void merge(Object aMerged, Object aPersistent) {
+        processPersistent(aMerged, aPersistent, new ArrayList<ObjectElem>());
+    }
+
+    /**
+     * Copies primary keys and version from the result of the merged to the
+     * object that was passed to the merge operation. It does this by traversing
+     * the properties of the object. It copies the primary key and version for
+     * objects that implement {@link Persistent} and applies the same rules to
+     * objects in maps and sets as well (i.e. recursively).
+     * 
+     * @param aPersistent
+     *            Object whose primary key and version are to be set.
+     * @param aMerged
+     *            Object that was the result of the merge.
+     * @param aProcessed
+     *            List of already processed Persistent objects of the persistent
+     *            part.
+     * 
+     */
+    public static void processPersistent(Object aMerged, Object aPersistent,
+        List<ObjectElem> aProcessed) {
+        if ((aPersistent == null) && (aMerged == null)) {
+            return;
+        }
+
+        if ((aPersistent == null) || (aMerged == null)) {
+            throw new RuntimeException("persistent or merged object is null '" +
+                aPersistent + "'" + "  '" + aMerged + "'");
+        }
+
+        ObjectElem elem = new ObjectElem(aPersistent);
+
+        if (aProcessed.contains(elem)) {
+            return; // already processed.
+        }
+
+        aProcessed.add(elem);
+
+        LOG.debug("Setting pk/version on " + aPersistent + " from " + aMerged);
+
+        Persistent persistentWrapper = PersistentFactory.create(aPersistent);
+        Persistent mergedWrapper = PersistentFactory.create(aMerged);
+
+        if (persistentWrapper == null) {
+            // Not an entity so it is ignored.
+            return;
+        }
+
+        Serializable pk = persistentWrapper.getPrimaryKey();
+        boolean pkIsNull = false;
+        if (pk instanceof Number) {
+            if (((Number) pk).longValue() != 0l) {
+                pkIsNull = false;
+            } else {
+                pkIsNull = true;
+            }
+        } else {
+            pkIsNull = (pk == null);
+        }
+        if (!pkIsNull &&
+            !mergedWrapper.getPrimaryKey().equals(
+                persistentWrapper.getPrimaryKey())) {
+            throw new IllegalArgumentException(
+                "Mismatch between primary key values: " + aPersistent + " " +
+                    aMerged);
+        } else {
+            persistentWrapper.setPersistedVersion(mergedWrapper
+                .getPersistedVersion());
+            persistentWrapper.setPrimaryKey(mergedWrapper.getPrimaryKey());
+        }
+
+        List<Method> methods = ReflectionUtils.getAllMethods(aPersistent
+            .getClass(), Object.class);
+
+        for (Method getter : methods) {
+            if ((getter.getName().startsWith("get") || getter.getName()
+                .startsWith("is")) &&
+                !Modifier.isStatic(getter.getModifiers())) {
+                Class returnType = getter.getReturnType();
+
+                try {
+                    if (Set.class.isAssignableFrom(returnType)) {
+                        Set merged = (Set) getter.invoke(aMerged);
+                        Set persistent = (Set) getter.invoke(aPersistent);
+                        processSet(merged, persistent, aProcessed);
+                    } else if (List.class.isAssignableFrom(returnType)) {
+                        List merged = (List) getter.invoke(aMerged);
+                        List persistent = (List) getter.invoke(aPersistent);
+                        processList(merged, persistent, aProcessed);
+                    } else if (Map.class.isAssignableFrom(returnType)) {
+                        Map merged = (Map) getter.invoke(aMerged);
+                        Map persistent = (Map) getter.invoke(aPersistent);
+                        processMap(merged, persistent, aProcessed);
+                    } else if (returnType.isArray()) {
+                        // early detection of whether it is an array of entities
+                        // to avoid performance problems.
+                        EntityAccessor accessor = PersistentFactory
+                            .createEntityAccessor(returnType.getComponentType());
+                        if (accessor != null) {
+                            Object[] merged = (Object[]) getter.invoke(aMerged);
+                            Object[] persistent = (Object[]) getter
+                                .invoke(aPersistent);
+                            if (merged.length != persistent.length) {
+                                throw new IllegalArgumentException("Array sizes differ " + merged.length +
+                                    " " + persistent.length);
+                            }
+                            for (int i = 0; i < persistent.length; i++) {
+                                processPersistent(merged[i], persistent[i],
+                                    aProcessed);
+                            }
+                        }
+                    } else {
+                        Object merged = getter.invoke(aMerged);
+                        Object persistent = getter.invoke(aPersistent);
+                        processPersistent(merged, persistent, aProcessed);
+                    }
+                } catch (InvocationTargetException e) {
+                    throw new RuntimeException(e.getMessage(), e);
+                } catch (IllegalAccessException e) {
+                    throw new RuntimeException(e.getMessage(), e);
+                }
+            }
+        }
+    }
+
+    /**
+     * Process the persistent objects in the collections.
+     * 
+     * @param aPersistent
+     *            Collection in the original object.
+     * @param aMerged
+     *            Collection as a result of the merge.
+     * @param aProcessed
+     *            List of processed persistent objects.
+     * 
+     */
+    public static void processList(List aMerged, List aPersistent,
+        List<ObjectElem> aProcessed) {
+        Object[] merged = aMerged.toArray();
+        Object[] persistent = aPersistent.toArray();
+
+        if (merged.length != persistent.length) {
+            throw new IllegalArgumentException("Array sizes differ " + merged.length +
+                " " + persistent.length);
+        }
+
+        for (int i = 0; i < merged.length; i++) {
+            assert merged[i].equals(persistent[i]);
+            processPersistent(merged[i], persistent[i], aProcessed);
+        }
+    }
+
+    /**
+     * Process the persistent objects in sets.
+     * 
+     * @param aPersistent
+     *            Collection in the original object.
+     * @param aMerged
+     *            Collection as a result of the merge.
+     * @param aProcessed
+     *            List of processed persistent objects.
+     * 
+     */
+    public static void processSet(Set aMerged, Set aPersistent,
+        List<ObjectElem> aProcessed) {
+        if (aMerged.size() != aPersistent.size()) {
+            throw new IllegalArgumentException("Array sizes differ " + aMerged.size() +
+                " " + aPersistent.size());
+        }
+
+        for (Object merged : aMerged) {
+            // Find the object that equals the merged[i]
+            for (Object persistent : aPersistent) {
+                if (persistent.equals(merged)) {
+                    processPersistent(merged, persistent, aProcessed);
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Process the Map objects in the collections.
+     * 
+     * @param aPersistent
+     *            Collection in the original object.
+     * @param aMerged
+     *            Collection as a result of the merge.
+     * @param aProcessed
+     *            List of processed persistent objects.
+     * 
+     */
+    public static <Key, Value> void processMap(Map<Key, Value> aMerged,
+        Map<Key, Value> aPersistent, List<ObjectElem> aProcessed) {
+        if (aMerged.size() != aPersistent.size()) {
+            throw new IllegalArgumentException("Sizes differ " + aMerged.size() + " " +
+                aPersistent.size());
+        }
+
+        Set<Entry<Key, Value>> entries = aMerged.entrySet();
+
+        for (Entry<Key, Value> entry : entries) {
+            Key key = entry.getKey();
+            if (!aPersistent.containsKey(key)) {
+                throw new IllegalArgumentException("Key '" + key + "' not found");
+            }
+
+            Value mergedValue = entry.getValue();
+            Object persistentValue = aPersistent.get(key);
+
+            processPersistent(mergedValue, persistentValue, aProcessed);
+        }
+    }
+
+    /**
+     * This class provided an equality operation based on the object reference
+     * of the wrapped object. This is required because we cannto assume that the
+     * equals operation has any meaning for different types of persistent
+     * objects. This allows us to use the standard collection classes for
+     * detecting cyclic dependences and avoiding recursion.
+     */
+    private static final class ObjectElem {
+        private Object object;
+
+        public ObjectElem(Object aObject) {
+            object = aObject;
+        }
+
+        public boolean equals(Object aObj) {
+            if (aObj == null) {
+                return false;
+            }
+            if (!(aObj instanceof ObjectElem)) {
+                return false;
+            }
+            return ((ObjectElem) aObj).object == object;
+        }
+
+        public int hashCode() {
+            return object.hashCode();
+        }
+    }
+}
index 91fd2b380095a66707aa578b7a0efb29dfb16059..7dfb460bc60e244242e65dec0a46c9a77bb47698 100644 (file)
@@ -64,10 +64,11 @@ public class PersistentFactory {
         @Override
         public T get(Object aEntity) {
             try {
-                return (T) field.get(aEntity);
+                T value = (T) field.get(aEntity);
+                return value;
             } catch (Exception e) {
                 throw new RuntimeException(e);
-            } 
+            }
         }
 
         @Override
@@ -110,7 +111,7 @@ public class PersistentFactory {
                 setter.invoke(aEntity, aValue);
             } catch (Exception e) {
                 throw new RuntimeException(e);
-            } 
+            }
         }
 
         public Method getGetter() {
@@ -155,21 +156,33 @@ public class PersistentFactory {
 
         @Override
         public Serializable getPrimaryKey() {
-            return (Serializable)accessor.getPk().get(entity);
+            if (accessor == null || accessor.getPk() == null) {
+                return null;
+            }
+            return (Serializable) accessor.getPk().get(entity);
         }
 
         @Override
         public void setPrimaryKey(Serializable aKey) {
+            if (accessor == null || accessor.getPk() == null) {
+                return;
+            }
             accessor.getPk().set(entity, aKey);
-        } 
+        }
 
         @Override
         public Number getPersistedVersion() {
-            return (Number)accessor.getVersion().get(entity);
+            if ( accessor == null || accessor.getVersion() == null) { 
+                return null; 
+            }
+            return (Number) accessor.getVersion().get(entity);
         }
 
         @Override
         public void setPersistedVersion(Number aVersion) {
+            if ( accessor == null || accessor.getVersion() == null) { 
+                return; 
+            }
             accessor.getVersion().set(entity, aVersion);
         }
     }
@@ -232,7 +245,8 @@ public class PersistentFactory {
                 }
                 try {
                     Class returnType = method.getReturnType();
-                    Method setter = method.getDeclaringClass().getDeclaredMethod(setterName, returnType);
+                    Method setter = method.getDeclaringClass()
+                        .getDeclaredMethod(setterName, returnType);
                     return new PropertyAccessor(method, setter);
                 } catch (NoSuchMethodException e) {
                     throw new RuntimeException("Error obtaining setter for " +
@@ -244,15 +258,17 @@ public class PersistentFactory {
     }
 
     /**
-     * Creates the {@link Persistent} wrapper for interfacing with primary key and 
-     * version of the entity. 
-     * @param aEntity Entity to use. 
-     * @return Persistent object or null if this is not an entity. 
+     * Creates the {@link Persistent} wrapper for interfacing with primary key
+     * and version of the entity.
+     * 
+     * @param aEntity
+     *            Entity to use.
+     * @return Persistent object or null if this is not an entity.
      */
-    public static Persistent create(Object aEntity) { 
+    public static Persistent create(Object aEntity) {
         EntityAccessor accessor = createEntityAccessor(aEntity.getClass());
-        if ( accessor == null ) { 
-            return null; 
+        if (accessor == null) {
+            return null;
         }
         return new EntityObjectAccessor(aEntity, accessor);
     }
diff --git a/support/general/src/test/java/org/wamblee/persistence/JpaMergeSupportTest.java b/support/general/src/test/java/org/wamblee/persistence/JpaMergeSupportTest.java
new file mode 100644 (file)
index 0000000..3e92799
--- /dev/null
@@ -0,0 +1,363 @@
+/*
+ * Copyright 2005-2010 the original author or authors.
+ * 
+ * Licensed under the Apache License, Version 2.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://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.wamblee.persistence;
+
+import static junit.framework.Assert.*;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.persistence.Id;
+import javax.persistence.Version;
+
+import org.junit.Test;
+
+public class JpaMergeSupportTest {
+
+    private static class X1 {
+        @Id
+        int id;
+
+        @Version
+        int version;
+
+        private String value;
+
+        public X1() {
+            value = "";
+        }
+
+        public X1(String aValue) {
+            value = aValue;
+        }
+
+        public String getValue() {
+            return value;
+        }
+
+        @Override
+        public boolean equals(Object aObj) {
+            if (aObj == null) {
+                return false;
+            }
+            if (!(aObj instanceof X1)) {
+                return false;
+            }
+            return value.equals(((X1) aObj).getValue());
+        }
+    }
+
+    private static class X2 {
+        @Id
+        int id;
+
+        private List<X1> list;
+
+        public X2() {
+            list = new ArrayList<X1>();
+        }
+
+        public List<X1> getList() {
+            return list;
+        }
+    }
+
+    private static class X3 {
+        @Id
+        int id;
+
+        private Set<X1> set;
+
+        public X3() {
+            set = new HashSet<X1>();
+        }
+
+        public Set<X1> getSet() {
+            return set;
+        }
+    }
+
+    private static class X4 {
+        @Id
+        int id;
+
+        private Map<String, X1> map;
+
+        public X4() {
+            map = new HashMap<String, X1>();
+        }
+
+        public Map<String, X1> getMap() {
+            return map;
+        }
+    }
+
+    private static class X5 {
+        @Id
+        int id;
+
+        private X1[] array;
+
+        public X5() {
+            // Empty.
+        }
+
+        public void setArray(X1[] aArray) {
+            array = aArray;
+        }
+
+        public X1[] getArray() {
+            return array;
+        }
+    }
+
+    @Test
+    public void testSimple() {
+        X1 x = new X1();
+        x.id = 10;
+        x.version = 20;
+
+        X1 y = new X1();
+
+        JpaMergeSupport.merge(x, y);
+
+        assertEquals(x.id, y.id);
+        assertEquals(x.version, y.version);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testSimplePkMismatch() {
+        X1 x = new X1();
+        x.id = 10;
+        x.version = 20;
+
+        X1 y = new X1();
+        y.id = 5;
+        JpaMergeSupport.merge(x, y);
+    }
+
+    @Test
+    public void testTraverseList() {
+        X2 x = new X2();
+        x.id = 10;
+        X1 a = new X1();
+        a.id = 20;
+        a.version = 21;
+        X1 b = new X1();
+        b.id = 30;
+        b.version = 31;
+
+        x.getList().add(a);
+        x.getList().add(b);
+
+        X2 y = new X2();
+        y.getList().add(new X1());
+        y.getList().add(new X1());
+
+        JpaMergeSupport.merge(x, y);
+
+        assertEquals(x.id, y.id);
+        assertEquals(x.getList().get(0).id, y.getList().get(0).id);
+        assertEquals(x.getList().get(1).id, y.getList().get(1).id);
+        assertEquals(x.getList().get(0).version, y.getList().get(0).version);
+        assertEquals(x.getList().get(1).version, y.getList().get(1).version);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testTraverseListWrongSize() {
+        X2 x = new X2();
+        x.id = 10;
+        X1 a = new X1();
+        a.id = 20;
+        a.version = 21;
+        X1 b = new X1();
+        b.id = 30;
+        b.version = 31;
+
+        x.getList().add(a);
+        x.getList().add(b);
+
+        X2 y = new X2();
+        y.getList().add(new X1());
+
+        JpaMergeSupport.merge(x, y);
+    }
+
+    @Test
+    public void testTraverseSet() {
+        X3 x = new X3();
+        x.id = 10;
+        X1 a = new X1("a");
+        a.id = 20;
+        a.version = 21;
+        X1 b = new X1("b");
+        b.id = 30;
+        b.version = 21;
+        x.getSet().add(a);
+        x.getSet().add(b);
+
+        X3 y = new X3();
+        X1 ya = new X1("a");
+        X1 yb = new X1("b");
+
+        y.getSet().add(ya);
+        y.getSet().add(yb);
+        JpaMergeSupport.merge(x, y);
+        assertEquals(x.id, y.id);
+        assertEquals(a.id, ya.id);
+        assertEquals(a.version, ya.version);
+        assertEquals(b.version, yb.version);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testTraverseSetWrongSize() {
+        X3 x = new X3();
+        x.id = 10;
+        X1 a = new X1("a");
+        a.id = 20;
+        a.version = 21;
+        X1 b = new X1("b");
+        b.id = 30;
+        b.version = 21;
+        x.getSet().add(a);
+        x.getSet().add(b);
+
+        X3 y = new X3();
+        X1 ya = new X1("a");
+        X1 yb = new X1("b");
+
+        y.getSet().add(ya);
+        JpaMergeSupport.merge(x, y);
+    }
+
+    @Test
+    public void testTraverseMap() {
+        X4 x = new X4();
+        x.id = 10;
+        X1 a = new X1("a");
+        a.id = 20;
+        a.version = 21;
+        X1 b = new X1("b");
+        b.id = 30;
+        b.version = 21;
+        x.getMap().put("a", a);
+        x.getMap().put("b", b);
+
+        X4 y = new X4();
+        X1 ya = new X1("a");
+        X1 yb = new X1("b");
+
+        y.getMap().put("a", ya);
+        y.getMap().put("b", yb);
+        JpaMergeSupport.merge(x, y);
+        assertEquals(x.id, y.id);
+        assertEquals(a.id, ya.id);
+        assertEquals(a.version, ya.version);
+        assertEquals(b.version, yb.version);
+
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testTraverseMapWrongKey() {
+        X4 x = new X4();
+        x.id = 10;
+        X1 a = new X1("a");
+        a.id = 20;
+        a.version = 21;
+        X1 b = new X1("b");
+        b.id = 30;
+        b.version = 21;
+        x.getMap().put("a", a);
+        x.getMap().put("b", b);
+
+        X4 y = new X4();
+        X1 ya = new X1("a");
+        X1 yb = new X1("b");
+
+        y.getMap().put("a", ya);
+        y.getMap().put("c", yb);
+        JpaMergeSupport.merge(x, y);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testTraverseMapWrongSize() {
+        X4 x = new X4();
+        x.id = 10;
+        X1 a = new X1("a");
+        a.id = 20;
+        a.version = 21;
+        X1 b = new X1("b");
+        b.id = 30;
+        b.version = 21;
+        x.getMap().put("a", a);
+        x.getMap().put("b", b);
+
+        X4 y = new X4();
+        X1 ya = new X1("a");
+        X1 yb = new X1("b");
+
+        y.getMap().put("a", ya);
+        JpaMergeSupport.merge(x, y);
+    }
+
+    @Test
+    public void testTraverseArray() {
+        X5 x = new X5();
+        x.id = 10;
+        X1 a = new X1("a");
+        a.id = 20;
+        a.version = 21;
+        X1 b = new X1("b");
+        b.id = 30;
+        b.version = 21;
+        x.setArray(new X1[] { a, b });
+
+        X5 y = new X5();
+        X1 ya = new X1("a");
+        X1 yb = new X1("b");
+
+        y.setArray(new X1[] { ya, yb });
+        JpaMergeSupport.merge(x, y);
+        assertEquals(x.id, y.id);
+        assertEquals(a.id, ya.id);
+        assertEquals(a.version, ya.version);
+        assertEquals(b.version, yb.version);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testTraverseArrayWrongSize() {
+        X5 x = new X5();
+        x.id = 10;
+        X1 a = new X1("a");
+        a.id = 20;
+        a.version = 21;
+        X1 b = new X1("b");
+        b.id = 30;
+        b.version = 21;
+        x.setArray(new X1[] { a, b });
+
+        X5 y = new X5();
+        X1 ya = new X1("a");
+
+        y.setArray(new X1[] { ya });
+        JpaMergeSupport.merge(x, y);
+    }
+
+}
index 9f254c694f53cf6f0b6c55b3e6d904a25a2eceac..16979de7a8a7d81d68513bb90601c430d02ba95d 100644 (file)
@@ -1,3 +1,18 @@
+/*
+ * Copyright 2005-2010 the original author or authors.
+ * 
+ * Licensed under the Apache License, Version 2.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://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package org.wamblee.persistence;
 
 import static junit.framework.Assert.*;
@@ -8,6 +23,7 @@ import javax.persistence.Version;
 import org.junit.Test;
 import org.wamblee.persistence.PersistentFactory.Accessor;
 import org.wamblee.persistence.PersistentFactory.EntityAccessor;
+import org.wamblee.persistence.PersistentFactory.EntityObjectAccessor;
 import org.wamblee.persistence.PersistentFactory.FieldAccessor;
 import org.wamblee.persistence.PersistentFactory.PropertyAccessor;
 
@@ -251,4 +267,16 @@ public class PersistentFactoryTest {
         
         assertSame(accessor, accessor2);
     }
+    
+    // EntityObjectAccessor test for undefined pk and/or version. 
+    @Test
+    public void testEntityObjectAccessorRobustness() { 
+        EntityObjectAccessor accessor =  new EntityObjectAccessor("hello world", 
+            new EntityAccessor(null, null));
+        assertNull(accessor.getPrimaryKey());
+        assertNull(accessor.getPersistedVersion());
+        accessor.setPrimaryKey("bla");
+        accessor.setPersistedVersion(100);
+        
+    }
 }