--- /dev/null
+/*
+ * 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();
+ }
+ }
+}
@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
setter.invoke(aEntity, aValue);
} catch (Exception e) {
throw new RuntimeException(e);
- }
+ }
}
public Method getGetter() {
@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);
}
}
}
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 " +
}
/**
- * 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);
}
--- /dev/null
+/*
+ * 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);
+ }
+
+}