5105d767b9772753ccdea1b0e328608b2cccf49d
[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 import java.util.logging.Logger;
28
29 import javax.persistence.EntityManager;
30
31 import org.wamblee.reflection.ReflectionUtils;
32
33 /**
34  * Support for merging of JPA entities. This utility allows the result of a
35  * merge (modifications of primary key and/or version) to be merged back into
36  * the argument that was merged. As a result, the merged entity can be reused
37  * and the application is not forced to use the new version that was returned
38  * from the merge.
39  * 
40  * The utility traverses the object graph based on the public getter methods.
41  * Therefore, care should be taken with this utility as usage could lead to
42  * recursively loading all objects reachable from the given object. Then again,
43  * this utility is for working with detached objects and it would, in general,
44  * be bad practice to work with detached objects that still contain unresolved
45  * lazy loaded relations and with detached objects that implicitly refer to
46  * almost the entire datamodel.
47  * 
48  * This utility best supports a service oriented design where interaction is
49  * through service interfaces where each service has its own storage isolated
50  * from other services. That would be opposed to a shared data design with many
51  * services acting on the same data.
52  * 
53  * @author Erik Brakkee
54  */
55 public class JpaMergeSupport {
56     private static final Logger LOG = Logger.getLogger(JpaMergeSupport.class.getName());
57
58     /**
59      * Constructs the object.
60      * 
61      */
62     public JpaMergeSupport() {
63         // Empty
64     }
65
66     /**
67      * As {@link #merge(Persistent)} but with a given template. This method can
68      * be accessed in a static way.
69      * 
70      * @param aMerge
71      *            The result of the call to {@link EntityManager#merge(Object)}.
72      * @param aPersistent
73      *            Object that was passed to {@link EntityManager#merge(Object)}.
74      */
75     public static void merge(Object aMerged, Object aPersistent) {
76         processPersistent(aMerged, aPersistent, new ArrayList<ObjectElem>());
77     }
78
79     /**
80      * Copies primary keys and version from the result of the merged to the
81      * object that was passed to the merge operation. It does this by traversing
82      * the public properties of the object. It copies the primary key and
83      * version for objects that implement {@link Persistent} and applies the
84      * same rules to objects in maps and sets as well (i.e. recursively).
85      * 
86      * @param aPersistent
87      *            Object whose primary key and version are to be set.
88      * @param aMerged
89      *            Object that was the result of the merge.
90      * @param aProcessed
91      *            List of already processed Persistent objects of the persistent
92      *            part.
93      * 
94      */
95     public static void processPersistent(Object aMerged, Object aPersistent,
96         List<ObjectElem> aProcessed) {
97         if ((aPersistent == null) && (aMerged == null)) {
98             return;
99         }
100
101         if ((aPersistent == null) || (aMerged == null)) {
102             throw new RuntimeException("persistent or merged object is null '" +
103                 aPersistent + "'" + "  '" + aMerged + "'");
104         }
105
106         ObjectElem elem = new ObjectElem(aPersistent);
107
108         if (aProcessed.contains(elem)) {
109             return; // already processed.
110         }
111
112         aProcessed.add(elem);
113
114         LOG.fine("Setting pk/version on " + aPersistent + " from " + aMerged);
115
116         Persistent persistentWrapper = PersistentFactory.create(aPersistent);
117         Persistent mergedWrapper = PersistentFactory.create(aMerged);
118
119         if (persistentWrapper == null) {
120             // Not an entity so it is ignored.
121             return;
122         }
123
124         Serializable pk = persistentWrapper.getPrimaryKey();
125         boolean pkIsNull = false;
126         if (pk instanceof Number) {
127             if (((Number) pk).longValue() != 0l) {
128                 pkIsNull = false;
129             } else {
130                 pkIsNull = true;
131             }
132         } else {
133             pkIsNull = (pk == null);
134         }
135         if (!pkIsNull &&
136             !mergedWrapper.getPrimaryKey().equals(
137                 persistentWrapper.getPrimaryKey())) {
138             throw new IllegalArgumentException(
139                 "Mismatch between primary key values: " + aPersistent + " " +
140                     aMerged);
141         }
142         persistentWrapper.setPersistedVersion(mergedWrapper
143             .getPersistedVersion());
144         persistentWrapper.setPrimaryKey(mergedWrapper.getPrimaryKey());
145
146         List<Method> methods = ReflectionUtils.getAllMethods(aPersistent
147             .getClass(), Object.class);
148
149         for (Method getter : methods) {
150             if ((getter.getName().startsWith("get") || getter.getName()
151                 .startsWith("is")) &&
152                 !Modifier.isStatic(getter.getModifiers()) &&
153                 Modifier.isPublic(getter.getModifiers()) &&
154                 getter.getParameterTypes().length == 0 &&
155                 getter.getReturnType() != Void.class) {
156                 Class returnType = getter.getReturnType();
157
158                 try {
159                     if (Set.class.isAssignableFrom(returnType)) {
160                         Set merged = (Set) getter.invoke(aMerged);
161                         Set persistent = (Set) getter.invoke(aPersistent);
162                         processSet(merged, persistent, aProcessed);
163                     } else if (List.class.isAssignableFrom(returnType)) {
164                         List merged = (List) getter.invoke(aMerged);
165                         List persistent = (List) getter.invoke(aPersistent);
166                         processList(merged, persistent, aProcessed);
167                     } else if (Map.class.isAssignableFrom(returnType)) {
168                         Map merged = (Map) getter.invoke(aMerged);
169                         Map persistent = (Map) getter.invoke(aPersistent);
170                         processMap(merged, persistent, aProcessed);
171                     } else if (returnType.isArray()) {
172                         Object[] merged = (Object[]) getter.invoke(aMerged);
173                         Object[] persistent = (Object[]) getter
174                             .invoke(aPersistent);
175                         if (merged.length != persistent.length) {
176                             throw new IllegalArgumentException(
177                                 "Array sizes differ " + merged.length + " " +
178                                     persistent.length);
179                         }
180                         for (int i = 0; i < persistent.length; i++) {
181                             processPersistent(merged[i], persistent[i],
182                                 aProcessed);
183                         }
184                     } else {
185                         Object merged = getter.invoke(aMerged);
186                         Object persistent = getter.invoke(aPersistent);
187                         processPersistent(merged, persistent, aProcessed);
188                     }
189                 } catch (InvocationTargetException e) {
190                     throw new RuntimeException(e.getMessage(), e);
191                 } catch (IllegalAccessException e) {
192                     throw new RuntimeException(e.getMessage(), e);
193                 }
194             }
195         }
196     }
197
198     /**
199      * Process the persistent objects in the collections.
200      * 
201      * @param aPersistent
202      *            Collection in the original object.
203      * @param aMerged
204      *            Collection as a result of the merge.
205      * @param aProcessed
206      *            List of processed persistent objects.
207      * 
208      */
209     public static void processList(List aMerged, List aPersistent,
210         List<ObjectElem> aProcessed) {
211         Object[] merged = aMerged.toArray();
212         Object[] persistent = aPersistent.toArray();
213
214         if (merged.length != persistent.length) {
215             throw new IllegalArgumentException("Array sizes differ " +
216                 merged.length + " " + persistent.length);
217         }
218
219         for (int i = 0; i < merged.length; i++) {
220             assert merged[i].equals(persistent[i]);
221             processPersistent(merged[i], persistent[i], aProcessed);
222         }
223     }
224
225     /**
226      * Process the persistent objects in sets.
227      * 
228      * @param aPersistent
229      *            Collection in the original object.
230      * @param aMerged
231      *            Collection as a result of the merge.
232      * @param aProcessed
233      *            List of processed persistent objects.
234      * 
235      */
236     public static void processSet(Set aMerged, Set aPersistent,
237         List<ObjectElem> aProcessed) {
238         if (aMerged.size() != aPersistent.size()) {
239             throw new IllegalArgumentException("Array sizes differ " +
240                 aMerged.size() + " " + aPersistent.size());
241         }
242
243         for (Object merged : aMerged) {
244             // Find the object that equals the merged[i]
245             for (Object persistent : aPersistent) {
246                 if (persistent.equals(merged)) {
247                     processPersistent(merged, persistent, aProcessed);
248                     break;
249                 }
250             }
251         }
252     }
253
254     /**
255      * Process the Map objects in the collections.
256      * 
257      * @param aPersistent
258      *            Collection in the original object.
259      * @param aMerged
260      *            Collection as a result of the merge.
261      * @param aProcessed
262      *            List of processed persistent objects.
263      * 
264      */
265     public static <Key, Value> void processMap(Map<Key, Value> aMerged,
266         Map<Key, Value> aPersistent, List<ObjectElem> aProcessed) {
267         if (aMerged.size() != aPersistent.size()) {
268             throw new IllegalArgumentException("Sizes differ " +
269                 aMerged.size() + " " + aPersistent.size());
270         }
271
272         Set<Entry<Key, Value>> entries = aMerged.entrySet();
273
274         for (Entry<Key, Value> entry : entries) {
275             Key key = entry.getKey();
276             if (!aPersistent.containsKey(key)) {
277                 throw new IllegalArgumentException("Key '" + key +
278                     "' not found");
279             }
280
281             Value mergedValue = entry.getValue();
282             Object persistentValue = aPersistent.get(key);
283
284             processPersistent(mergedValue, persistentValue, aProcessed);
285         }
286     }
287
288     /**
289      * This class provided an equality operation based on the object reference
290      * of the wrapped object. This is required because we cannto assume that the
291      * equals operation has any meaning for different types of persistent
292      * objects. This allows us to use the standard collection classes for
293      * detecting cyclic dependences and avoiding recursion.
294      */
295     private static final class ObjectElem {
296         private Object object;
297
298         public ObjectElem(Object aObject) {
299             object = aObject;
300         }
301
302         public boolean equals(Object aObj) {
303             if (aObj == null) {
304                 return false;
305             }
306             if (!(aObj instanceof ObjectElem)) {
307                 return false;
308             }
309             return ((ObjectElem) aObj).object == object;
310         }
311
312         public int hashCode() {
313             return object.hashCode();
314         }
315     }
316 }