(no commit message)
[utils] / support / general / src / main / java / org / wamblee / persistence / JpaMergeSupport.java
1 /*
2  * Copyright 2005-2010 the original author or authors.
3  * 
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
7  * 
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  * 
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.
15  */
16 package org.wamblee.persistence;
17
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;
24 import java.util.Map;
25 import java.util.Set;
26 import java.util.Map.Entry;
27
28 import javax.persistence.EntityManager;
29
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;
34
35 /**
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
40  * from the merge.
41  * 
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.
49  * 
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.
54  * 
55  * @author Erik Brakkee
56  */
57 public class JpaMergeSupport {
58     private static final Log LOG = LogFactory.getLog(JpaMergeSupport.class);
59
60     /**
61      * Constructs the object.
62      * 
63      */
64     public JpaMergeSupport() {
65         // Empty
66     }
67
68     /**
69      * As {@link #merge(Persistent)} but with a given template. This method can
70      * be accessed in a static way.
71      * 
72      * @param aMerge
73      *            The result of the call to {@link EntityManager#merge(Object)}.
74      * @param aPersistent
75      *            Object that was passed to {@link EntityManager#merge(Object)}.
76      */
77     public static void merge(Object aMerged, Object aPersistent) {
78         processPersistent(aMerged, aPersistent, new ArrayList<ObjectElem>());
79     }
80
81     /**
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).
87      * 
88      * @param aPersistent
89      *            Object whose primary key and version are to be set.
90      * @param aMerged
91      *            Object that was the result of the merge.
92      * @param aProcessed
93      *            List of already processed Persistent objects of the persistent
94      *            part.
95      * 
96      */
97     public static void processPersistent(Object aMerged, Object aPersistent,
98         List<ObjectElem> aProcessed) {
99         if ((aPersistent == null) && (aMerged == null)) {
100             return;
101         }
102
103         if ((aPersistent == null) || (aMerged == null)) {
104             throw new RuntimeException("persistent or merged object is null '" +
105                 aPersistent + "'" + "  '" + aMerged + "'");
106         }
107
108         ObjectElem elem = new ObjectElem(aPersistent);
109
110         if (aProcessed.contains(elem)) {
111             return; // already processed.
112         }
113
114         aProcessed.add(elem);
115
116         LOG.debug("Setting pk/version on " + aPersistent + " from " + aMerged);
117
118         Persistent persistentWrapper = PersistentFactory.create(aPersistent);
119         Persistent mergedWrapper = PersistentFactory.create(aMerged);
120
121         if (persistentWrapper == null) {
122             // Not an entity so it is ignored.
123             return;
124         }
125
126         Serializable pk = persistentWrapper.getPrimaryKey();
127         boolean pkIsNull = false;
128         if (pk instanceof Number) {
129             if (((Number) pk).longValue() != 0l) {
130                 pkIsNull = false;
131             } else {
132                 pkIsNull = true;
133             }
134         } else {
135             pkIsNull = (pk == null);
136         }
137         if (!pkIsNull &&
138             !mergedWrapper.getPrimaryKey().equals(
139                 persistentWrapper.getPrimaryKey())) {
140             throw new IllegalArgumentException(
141                 "Mismatch between primary key values: " + aPersistent + " " +
142                     aMerged);
143         } else {
144             persistentWrapper.setPersistedVersion(mergedWrapper
145                 .getPersistedVersion());
146             persistentWrapper.setPrimaryKey(mergedWrapper.getPrimaryKey());
147         }
148
149         List<Method> methods = ReflectionUtils.getAllMethods(aPersistent
150             .getClass(), Object.class);
151
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
159                 ) {
160                 Class returnType = getter.getReturnType();
161
162                 try {
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);
187                             }
188                             for (int i = 0; i < persistent.length; i++) {
189                                 processPersistent(merged[i], persistent[i],
190                                     aProcessed);
191                             }
192                         }
193                     } else {
194                         Object merged = getter.invoke(aMerged);
195                         Object persistent = getter.invoke(aPersistent);
196                         processPersistent(merged, persistent, aProcessed);
197                     }
198                 } catch (InvocationTargetException e) {
199                     throw new RuntimeException(e.getMessage(), e);
200                 } catch (IllegalAccessException e) {
201                     throw new RuntimeException(e.getMessage(), e);
202                 }
203             }
204         }
205     }
206
207     /**
208      * Process the persistent objects in the collections.
209      * 
210      * @param aPersistent
211      *            Collection in the original object.
212      * @param aMerged
213      *            Collection as a result of the merge.
214      * @param aProcessed
215      *            List of processed persistent objects.
216      * 
217      */
218     public static void processList(List aMerged, List aPersistent,
219         List<ObjectElem> aProcessed) {
220         Object[] merged = aMerged.toArray();
221         Object[] persistent = aPersistent.toArray();
222
223         if (merged.length != persistent.length) {
224             throw new IllegalArgumentException("Array sizes differ " + merged.length +
225                 " " + persistent.length);
226         }
227
228         for (int i = 0; i < merged.length; i++) {
229             assert merged[i].equals(persistent[i]);
230             processPersistent(merged[i], persistent[i], aProcessed);
231         }
232     }
233
234     /**
235      * Process the persistent objects in sets.
236      * 
237      * @param aPersistent
238      *            Collection in the original object.
239      * @param aMerged
240      *            Collection as a result of the merge.
241      * @param aProcessed
242      *            List of processed persistent objects.
243      * 
244      */
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());
250         }
251
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);
257                     break;
258                 }
259             }
260         }
261     }
262
263     /**
264      * Process the Map objects in the collections.
265      * 
266      * @param aPersistent
267      *            Collection in the original object.
268      * @param aMerged
269      *            Collection as a result of the merge.
270      * @param aProcessed
271      *            List of processed persistent objects.
272      * 
273      */
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() + " " +
278                 aPersistent.size());
279         }
280
281         Set<Entry<Key, Value>> entries = aMerged.entrySet();
282
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");
287             }
288
289             Value mergedValue = entry.getValue();
290             Object persistentValue = aPersistent.get(key);
291
292             processPersistent(mergedValue, persistentValue, aProcessed);
293         }
294     }
295
296     /**
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.
302      */
303     private static final class ObjectElem {
304         private Object object;
305
306         public ObjectElem(Object aObject) {
307             object = aObject;
308         }
309
310         public boolean equals(Object aObj) {
311             if (aObj == null) {
312                 return false;
313             }
314             if (!(aObj instanceof ObjectElem)) {
315                 return false;
316             }
317             return ((ObjectElem) aObj).object == object;
318         }
319
320         public int hashCode() {
321             return object.hashCode();
322         }
323     }
324 }