/* * 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 java.util.logging.Logger; import javax.persistence.EntityManager; import org.wamblee.general.ObjectElem; 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 Logger LOG = Logger.getLogger(JpaMergeSupport.class .getName()); /** * 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()); } /** * 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 public 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 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.fine("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); } persistentWrapper.setPersistedVersion(mergedWrapper .getPersistedVersion()); persistentWrapper.setPrimaryKey(mergedWrapper.getPrimaryKey()); List methods = ReflectionUtils.getAllMethods(aPersistent .getClass(), Object.class); for (Method getter : methods) { if ((getter.getName().startsWith("get") || getter.getName() .startsWith("is")) && !Modifier.isStatic(getter.getModifiers()) && Modifier.isPublic(getter.getModifiers()) && getter.getParameterTypes().length == 0 && getter.getReturnType() != Void.class) { 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()) { 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 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 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 void processMap(Map aMerged, Map aPersistent, List aProcessed) { if (aMerged.size() != aPersistent.size()) { throw new IllegalArgumentException("Sizes differ " + aMerged.size() + " " + aPersistent.size()); } Set> entries = aMerged.entrySet(); for (Entry 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); } } }