From: Erik Brakkee Date: Fri, 30 Apr 2010 12:47:01 +0000 (+0000) Subject: Jpa merge support is now available. X-Git-Tag: wamblee-utils-0.7~503 X-Git-Url: http://wamblee.org/gitweb/?a=commitdiff_plain;h=820c55dae3f92efe95ff8a3f4a9b5ea474f7d8d0;p=utils Jpa merge support is now available. --- 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 index 00000000..be188e2d --- /dev/null +++ b/support/general/src/main/java/org/wamblee/persistence/JpaMergeSupport.java @@ -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()); + } + + /** + * 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 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 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 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 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 void processMap(Map aMerged, + Map aPersistent, List aProcessed) { + if (aMerged.size() != aPersistent.size()) { + throw new IllegalArgumentException("Sizes differ " + aMerged.size() + " " + + aPersistent.size()); + } + + Set> entries = aMerged.entrySet(); + + for (Entry 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(); + } + } +} diff --git a/support/general/src/main/java/org/wamblee/persistence/PersistentFactory.java b/support/general/src/main/java/org/wamblee/persistence/PersistentFactory.java index 91fd2b38..7dfb460b 100644 --- a/support/general/src/main/java/org/wamblee/persistence/PersistentFactory.java +++ b/support/general/src/main/java/org/wamblee/persistence/PersistentFactory.java @@ -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 index 00000000..3e92799b --- /dev/null +++ b/support/general/src/test/java/org/wamblee/persistence/JpaMergeSupportTest.java @@ -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 list; + + public X2() { + list = new ArrayList(); + } + + public List getList() { + return list; + } + } + + private static class X3 { + @Id + int id; + + private Set set; + + public X3() { + set = new HashSet(); + } + + public Set getSet() { + return set; + } + } + + private static class X4 { + @Id + int id; + + private Map map; + + public X4() { + map = new HashMap(); + } + + public Map 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); + } + +} diff --git a/support/general/src/test/java/org/wamblee/persistence/PersistentFactoryTest.java b/support/general/src/test/java/org/wamblee/persistence/PersistentFactoryTest.java index 9f254c69..16979de7 100644 --- a/support/general/src/test/java/org/wamblee/persistence/PersistentFactoryTest.java +++ b/support/general/src/test/java/org/wamblee/persistence/PersistentFactoryTest.java @@ -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); + + } }