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