package sk.nociar.jpacloner; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import javax.persistence.Embeddable; import javax.persistence.Entity; import sk.nociar.jpacloner.graphs.GraphExplorer; /** * JpaCloner provides cloning of JPA entity subgraphs. Cloned entities will be instantiated as <b>raw classes</b>. * The <b>raw class</b> means a class annotated by {@link Entity} or {@link Embeddable}, not a Hibernate proxy. * String patterns define <b>included relations</b> which will be cloned. For description of patterns see the {@link GraphExplorer}. * Cloned entities will have all <b>basic properties</b> (non-relation properties) copied by default. * Advanced control over the cloning process is supported via the {@link PropertyFilter} interface. * There are two options for cloning:<br/><br/> * <ol> * <li> * Cloning without a {@link PropertyFilter}. All <b>basic properties</b> of entities are copied by default in this case: * <pre> * Company cloned = JpaCloner.clone(company, "department+.(boss|employees).address");</pre> * </li> * <li> * Cloning with a {@link PropertyFilter}. The {@link PropertyFilter} implementation serves as an exclusion filter * of <b>relations</b> and <b>basic properties</b>: * <pre> * PropertyFilter filter = new PropertyFilter() { * public boolean test(Object entity, String property) { * // do not clone primary keys * return !"id".equals(property); * } * } * Company cloned = JpaCloner.clone(company, filter, "department+.(boss|employees).address");</pre> * </li> * </ol> * Cloned <b>relations</b> will be standard java.util classes:<br/> * {@link Set}->{@link LinkedHashSet}<br/> * {@link Map}->{@link LinkedHashMap}<br/> * {@link List}->{@link ArrayList}<br/> * {@link SortedSet}->{@link TreeSet}<br/> * {@link SortedMap}->{@link TreeMap}<br/> * <br/> * Cloning of a {@link Map} is supported via "key" and "value" properties e.g. "my.map.(key.a.b.c|value.x.y.z)". * Please note that the cloning has also a side effect regarding the lazy loading. * All entities which will be cloned could be fetched from the DB. It is advisable * (but not required) to perform the cloning inside a <b>transaction scope</b>. * <br/><br/> * Requirements: * <ul> * <li>JPA entities must <b>correctly</b> implement the {@link Object#equals(Object obj)} * method and the {@link Object#hashCode()} method!</li> * </ul> * * @author Miroslav Nociar */ public final class JpaCloner { private JpaCloner() { throw new UnsupportedOperationException(); } /** * Clones all explored entities and relations. * @param explorer * @return map of original -> clone */ @SuppressWarnings({ "rawtypes", "unchecked" }) private static Map<Object, Object> clone(JpaExplorer explorer, PropertyFilter propertyFilter) { Map<Object, Object> originalToClone = new HashMap<Object, Object>(explorer.entities.size()); // clone each explored JPA entity for (Object original : explorer.entities.keySet()) { JpaClassInfo classInfo = JpaClassInfo.get(original.getClass()); Object clone; try { clone = classInfo.getConstructor().newInstance(); } catch (Exception e) { throw new IllegalStateException("Unable to clone: " + original, e); } // copy basic properties copyBasicProperties(original, clone, classInfo, propertyFilter); // put in the cache originalToClone.put(original, clone); } // clone @ManyToOne, @OneToOne, @Embedded, @EmbeddedId for (Map.Entry<Object, Set<String>> entry : explorer.entities.entrySet()) { Object original = entry.getKey(); Set<String> relations = entry.getValue(); Object clone = originalToClone.get(original); JpaClassInfo classInfo = JpaClassInfo.get(original.getClass()); for (String relation : relations) { JpaPropertyInfo propertyInfo = classInfo.getPropertyInfo(relation); if (propertyInfo.isSingular()) { Object originalValue = propertyInfo.getValue(original); Object clonedValue = originalToClone.get(originalValue); propertyInfo.setValue(clone, clonedValue); } } } // clone @OneToMany, @ManyToMany, @ElementCollection for (Map.Entry<Object, Set<String>> entry : explorer.entities.entrySet()) { Object original = entry.getKey(); Set<String> relations = entry.getValue(); Object clone = originalToClone.get(original); JpaClassInfo classInfo = JpaClassInfo.get(original.getClass()); for (String relation : relations) { JpaPropertyInfo propertyInfo = classInfo.getPropertyInfo(relation); if (propertyInfo.isSingular()) { continue; } Object originalValue = propertyInfo.getValue(original); if (originalValue instanceof Collection) { Collection originalCollection = (Collection) originalValue; Collection clonedCollection; if (originalCollection instanceof SortedSet) { // TreeSet with the same Comparator (can be null) clonedCollection = new TreeSet(((SortedSet) originalValue).comparator()); } else if (originalCollection instanceof Set) { // HashSet clonedCollection = new LinkedHashSet(originalCollection.size()); } else if (originalCollection instanceof List) { // ArrayList clonedCollection = new ArrayList(originalCollection.size()); } else { throw new IllegalStateException("Unsupported collection type: " + originalValue.getClass()); } for (Object o : originalCollection) { Object c = originalToClone.get(o); if (c == null) { c = o; } clonedCollection.add(c); } propertyInfo.setValue(clone, clonedCollection); } else if (originalValue instanceof Map) { Map originalMap = (Map) originalValue; Map clonedMap; if (originalMap instanceof SortedMap) { clonedMap = new TreeMap(((SortedMap) originalValue).comparator()); } else { clonedMap = new LinkedHashMap(originalMap.size()); } for (Object o : originalMap.entrySet()) { Entry e = (Entry) o; Object key = e.getKey(); Object value = e.getValue(); Object key2 = originalToClone.get(key); Object value2 = originalToClone.get(value); if (key2 == null) { key2 = key; } if (value2 == null) { value2 = value; } clonedMap.put(key2, value2); } propertyInfo.setValue(clone, clonedMap); } } } return originalToClone; } /** * Clones the passed JPA entity. The property filter controls the cloning of <b>basic properties</b>. * The cloned relations are specified by string patters. For description of patterns see the {@link GraphExplorer}. */ @SuppressWarnings("unchecked") public static <T> T clone(T root, PropertyFilter propertyFilter, String... patterns) { JpaExplorer explorer = JpaExplorer.doExplore(root, propertyFilter, patterns); return (T) clone(explorer, propertyFilter).get(root); } /** * Clones the list of JPA entities. The property filter controls the cloning of <b>basic properties</b>. * The cloned relations are specified by string patters. For description of patterns see the {@link GraphExplorer}. */ @SuppressWarnings("unchecked") public static <T> List<T> clone(Collection<T> list, PropertyFilter propertyFilter, String... patterns) { List<T> clonedList = new ArrayList<T>(list.size()); JpaExplorer explorer = JpaExplorer.doExplore(list, propertyFilter, patterns); Map<Object, Object> originalToClone = clone(explorer, propertyFilter); for (T original : list) { clonedList.add((T) originalToClone.get(original)); } return clonedList; } /** * Clones the set of JPA entities. The property filter controls the cloning of <b>basic properties</b>. * The cloned relations are specified by string patters. For description of patterns see the {@link GraphExplorer}. */ @SuppressWarnings("unchecked") public static <T> Set<T> clone(Set<T> set, PropertyFilter propertyFilter, String... patterns) { Set<T> clonedSet = new HashSet<T>(); JpaExplorer explorer = JpaExplorer.doExplore(set, propertyFilter, patterns); Map<Object, Object> originalToClone = clone(explorer, propertyFilter); for (T original : set) { clonedSet.add((T) originalToClone.get(original)); } return clonedSet; } /** * Clones the passed JPA entity. Each entity has <b>all basic properties</b> cloned. * The cloned relations are specified by string patters. For description of patterns see the {@link GraphExplorer}. */ public static <T> T clone(T root, String... patterns) { return clone(root, PropertyFilters.getDefaultFilter(), patterns); } /** * Clones the list of JPA entities. Each entity has <b>all basic properties</b> cloned. * The cloned relations are specified by string patters. For description of patterns see the {@link GraphExplorer}. */ public static <T> List<T> clone(Collection<T> list, String... patterns) { return clone(list, PropertyFilters.getDefaultFilter(), patterns); } /** * Clones the set of JPA entities. Each entity has <b>all basic properties</b> cloned. * The cloned relations are specified by string patters. For description of patterns see the {@link GraphExplorer}. */ public static <T> Set<T> clone(Set<T> set, String... patterns) { return clone(set, PropertyFilters.getDefaultFilter(), patterns); } /** * Copy properties (not relations) from o1 to o2. */ private static void copyBasicProperties(Object o1, Object o2, JpaClassInfo classInfo, PropertyFilter propertyFilter) { for (String property : classInfo.getBaseProperties()) { if (propertyFilter.test(o1, property)) { JpaPropertyInfo propertyInfo = classInfo.getPropertyInfo(property); Object value = propertyInfo.getValue(o1); propertyInfo.setValue(o2, value); } } } /** * Copy all <b>basic properties</b> from the first entity to the second entity. */ public static <T, X extends T> void copy(T o1, X o2) { copy(o1, o2, PropertyFilters.getDefaultFilter()); } /** * Copy filtered <b>basic properties</b> from the first entity to the second entity. */ public static <T, X extends T> void copy(T o1, X o2, PropertyFilter propertyFilter) { JpaClassInfo classInfo = JpaClassInfo.get(o1.getClass()); copyBasicProperties(o1, o2, classInfo, propertyFilter); } }