2 * Copyright 2005-2010 the original author or authors.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package org.wamblee.persistence;
18 import java.io.Serializable;
19 import java.lang.reflect.InvocationTargetException;
20 import java.lang.reflect.Method;
21 import java.lang.reflect.Modifier;
22 import java.util.ArrayList;
23 import java.util.List;
26 import java.util.Map.Entry;
28 import javax.persistence.EntityManager;
30 import org.apache.commons.logging.Log;
31 import org.apache.commons.logging.LogFactory;
32 import org.wamblee.persistence.PersistentFactory.EntityAccessor;
33 import org.wamblee.reflection.ReflectionUtils;
36 * Support for merging of JPA entities. This utility allows the result of a
37 * merge (modifications of primary key and/or version) to be merged back into
38 * the argument that was merged. As a result, the merged entity can be reused
39 * and the application is not forced to use the new version that was returned
42 * The utility traverses the object graph based on the public getter methods.
43 * Therefore, care should be taken with this utility as usage could lead to
44 * recursively loading all objects reachable from the given object. Then again,
45 * this utility is for working with detached objects and it would, in general,
46 * be bad practice to work with detached objects that still contain unresolved
47 * lazy loaded relations and with detached objects that implicitly refer to
48 * almost the entire datamodel.
50 * This utility best supports a service oriented design where interaction is
51 * through service interfaces where each service has its own storage isolated
52 * from other services. That would be opposed to a shared data design with many
53 * services acting on the same data.
55 * @author Erik Brakkee
57 public class JpaMergeSupport {
58 private static final Log LOG = LogFactory.getLog(JpaMergeSupport.class);
61 * Constructs the object.
64 public JpaMergeSupport() {
69 * As {@link #merge(Persistent)} but with a given template. This method can
70 * be accessed in a static way.
73 * The result of the call to {@link EntityManager#merge(Object)}.
75 * Object that was passed to {@link EntityManager#merge(Object)}.
77 public static void merge(Object aMerged, Object aPersistent) {
78 processPersistent(aMerged, aPersistent, new ArrayList<ObjectElem>());
82 * Copies primary keys and version from the result of the merged to the
83 * object that was passed to the merge operation. It does this by traversing
84 * the public properties of the object. It copies the primary key and version for
85 * objects that implement {@link Persistent} and applies the same rules to
86 * objects in maps and sets as well (i.e. recursively).
89 * Object whose primary key and version are to be set.
91 * Object that was the result of the merge.
93 * List of already processed Persistent objects of the persistent
97 public static void processPersistent(Object aMerged, Object aPersistent,
98 List<ObjectElem> aProcessed) {
99 if ((aPersistent == null) && (aMerged == null)) {
103 if ((aPersistent == null) || (aMerged == null)) {
104 throw new RuntimeException("persistent or merged object is null '" +
105 aPersistent + "'" + " '" + aMerged + "'");
108 ObjectElem elem = new ObjectElem(aPersistent);
110 if (aProcessed.contains(elem)) {
111 return; // already processed.
114 aProcessed.add(elem);
116 LOG.debug("Setting pk/version on " + aPersistent + " from " + aMerged);
118 Persistent persistentWrapper = PersistentFactory.create(aPersistent);
119 Persistent mergedWrapper = PersistentFactory.create(aMerged);
121 if (persistentWrapper == null) {
122 // Not an entity so it is ignored.
126 Serializable pk = persistentWrapper.getPrimaryKey();
127 boolean pkIsNull = false;
128 if (pk instanceof Number) {
129 if (((Number) pk).longValue() != 0l) {
135 pkIsNull = (pk == null);
138 !mergedWrapper.getPrimaryKey().equals(
139 persistentWrapper.getPrimaryKey())) {
140 throw new IllegalArgumentException(
141 "Mismatch between primary key values: " + aPersistent + " " +
144 persistentWrapper.setPersistedVersion(mergedWrapper
145 .getPersistedVersion());
146 persistentWrapper.setPrimaryKey(mergedWrapper.getPrimaryKey());
149 List<Method> methods = ReflectionUtils.getAllMethods(aPersistent
150 .getClass(), Object.class);
152 for (Method getter : methods) {
153 if ((getter.getName().startsWith("get") || getter.getName()
154 .startsWith("is")) &&
155 !Modifier.isStatic(getter.getModifiers()) &&
156 Modifier.isPublic(getter.getModifiers()) &&
157 getter.getParameterTypes().length == 0 &&
158 getter.getReturnType() != Void.class
160 Class returnType = getter.getReturnType();
163 if (Set.class.isAssignableFrom(returnType)) {
164 Set merged = (Set) getter.invoke(aMerged);
165 Set persistent = (Set) getter.invoke(aPersistent);
166 processSet(merged, persistent, aProcessed);
167 } else if (List.class.isAssignableFrom(returnType)) {
168 List merged = (List) getter.invoke(aMerged);
169 List persistent = (List) getter.invoke(aPersistent);
170 processList(merged, persistent, aProcessed);
171 } else if (Map.class.isAssignableFrom(returnType)) {
172 Map merged = (Map) getter.invoke(aMerged);
173 Map persistent = (Map) getter.invoke(aPersistent);
174 processMap(merged, persistent, aProcessed);
175 } else if (returnType.isArray()) {
176 // early detection of whether it is an array of entities
177 // to avoid performance problems.
178 EntityAccessor accessor = PersistentFactory
179 .createEntityAccessor(returnType.getComponentType());
180 if (accessor != null) {
181 Object[] merged = (Object[]) getter.invoke(aMerged);
182 Object[] persistent = (Object[]) getter
183 .invoke(aPersistent);
184 if (merged.length != persistent.length) {
185 throw new IllegalArgumentException("Array sizes differ " + merged.length +
186 " " + persistent.length);
188 for (int i = 0; i < persistent.length; i++) {
189 processPersistent(merged[i], persistent[i],
194 Object merged = getter.invoke(aMerged);
195 Object persistent = getter.invoke(aPersistent);
196 processPersistent(merged, persistent, aProcessed);
198 } catch (InvocationTargetException e) {
199 throw new RuntimeException(e.getMessage(), e);
200 } catch (IllegalAccessException e) {
201 throw new RuntimeException(e.getMessage(), e);
208 * Process the persistent objects in the collections.
211 * Collection in the original object.
213 * Collection as a result of the merge.
215 * List of processed persistent objects.
218 public static void processList(List aMerged, List aPersistent,
219 List<ObjectElem> aProcessed) {
220 Object[] merged = aMerged.toArray();
221 Object[] persistent = aPersistent.toArray();
223 if (merged.length != persistent.length) {
224 throw new IllegalArgumentException("Array sizes differ " + merged.length +
225 " " + persistent.length);
228 for (int i = 0; i < merged.length; i++) {
229 assert merged[i].equals(persistent[i]);
230 processPersistent(merged[i], persistent[i], aProcessed);
235 * Process the persistent objects in sets.
238 * Collection in the original object.
240 * Collection as a result of the merge.
242 * List of processed persistent objects.
245 public static void processSet(Set aMerged, Set aPersistent,
246 List<ObjectElem> aProcessed) {
247 if (aMerged.size() != aPersistent.size()) {
248 throw new IllegalArgumentException("Array sizes differ " + aMerged.size() +
249 " " + aPersistent.size());
252 for (Object merged : aMerged) {
253 // Find the object that equals the merged[i]
254 for (Object persistent : aPersistent) {
255 if (persistent.equals(merged)) {
256 processPersistent(merged, persistent, aProcessed);
264 * Process the Map objects in the collections.
267 * Collection in the original object.
269 * Collection as a result of the merge.
271 * List of processed persistent objects.
274 public static <Key, Value> void processMap(Map<Key, Value> aMerged,
275 Map<Key, Value> aPersistent, List<ObjectElem> aProcessed) {
276 if (aMerged.size() != aPersistent.size()) {
277 throw new IllegalArgumentException("Sizes differ " + aMerged.size() + " " +
281 Set<Entry<Key, Value>> entries = aMerged.entrySet();
283 for (Entry<Key, Value> entry : entries) {
284 Key key = entry.getKey();
285 if (!aPersistent.containsKey(key)) {
286 throw new IllegalArgumentException("Key '" + key + "' not found");
289 Value mergedValue = entry.getValue();
290 Object persistentValue = aPersistent.get(key);
292 processPersistent(mergedValue, persistentValue, aProcessed);
297 * This class provided an equality operation based on the object reference
298 * of the wrapped object. This is required because we cannto assume that the
299 * equals operation has any meaning for different types of persistent
300 * objects. This allows us to use the standard collection classes for
301 * detecting cyclic dependences and avoiding recursion.
303 private static final class ObjectElem {
304 private Object object;
306 public ObjectElem(Object aObject) {
310 public boolean equals(Object aObj) {
314 if (!(aObj instanceof ObjectElem)) {
317 return ((ObjectElem) aObj).object == object;
320 public int hashCode() {
321 return object.hashCode();