/* * Copyright (C) 2014-2016 University of Dundee & Open Microscopy Environment. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package omero.cmd.graphs; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.apache.commons.beanutils.NestedNullException; import org.apache.commons.beanutils.PropertyUtils; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.hibernate.Hibernate; import org.hibernate.Session; import org.hibernate.proxy.HibernateProxy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Function; import com.google.common.base.Functions; import com.google.common.base.Predicate; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.SetMultimap; import ome.model.IObject; import ome.security.ACLVoter; import ome.security.SystemTypes; import ome.services.delete.Deletion; import ome.services.graphs.GraphException; import ome.services.graphs.GraphPathBean; import ome.services.graphs.GraphPolicy; import ome.services.graphs.GraphTraversal; import ome.services.graphs.PermissionsPredicate; import ome.system.Roles; import omero.cmd.Duplicate; import omero.cmd.DuplicateResponse; import omero.cmd.HandleI.Cancel; import omero.cmd.ERR; import omero.cmd.Helper; import omero.cmd.IRequest; import omero.cmd.Response; /** * Request to duplicate model objects. * @author m.t.b.carroll@dundee.ac.uk * @since 5.2.1 */ public class DuplicateI extends Duplicate implements IRequest, WrappableRequest<Duplicate> { private static final Logger LOGGER = LoggerFactory.getLogger(DuplicateI.class); private static final Set<GraphPolicy.Ability> REQUIRED_ABILITIES = ImmutableSet.of(); /* all bulk operations are batched; this size should be suitable for IN (:ids) for HQL */ private static final int BATCH_SIZE = 256; private static enum Inclusion { /* the object is to be duplicated */ DUPLICATE, /* the object is not to be duplicated, but it may be referenced from duplicates */ REFERENCE, /* the object is not to be duplicated, nor is it to be referenced from duplicates */ IGNORE }; private final ACLVoter aclVoter; private final SystemTypes systemTypes; private final GraphPathBean graphPathBean; private final Set<Class<? extends IObject>> targetClasses; private GraphPolicy graphPolicy; /* not final because of adjustGraphPolicy */ private final SetMultimap<String, String> unnullable; /* a taxonomy based on the class hierarchy of model objects in Java */ private SpecificityClassifier<Class<? extends IObject>, Inclusion> classifier; private List<Function<GraphPolicy, GraphPolicy>> graphPolicyAdjusters = new ArrayList<Function<GraphPolicy, GraphPolicy>>(); private Helper helper; private GraphHelper graphHelper; private GraphTraversal graphTraversal; private GraphTraversal.PlanExecutor processor; private int targetObjectCount = 0; private int duplicatedObjectCount = 0; private final Map<IObject, IObject> originalsToDuplicates = new HashMap<IObject, IObject>(); private final Map<Entry<String, Long>, IObject> originalClassIdToDuplicates = new HashMap<Entry<String, Long>, IObject>(); private final Multimap<IObject, PropertyUpdate> propertiesToUpdate = ArrayListMultimap.create(); private final SetMultimap<IObject, IObject> blockedBy = HashMultimap.create(); /** * Given class names provided by the user, find the corresponding set of actual classes. * @param classNames names of model object classes * @return the named classes */ private Set<Class<? extends IObject>> getClassesFromNames(Collection<String> classNames) { if (CollectionUtils.isEmpty(classNames)) { return Collections.emptySet(); } final Set<Class<? extends IObject>> classes = new HashSet<Class<? extends IObject>>(); for (String className : classNames) { final int lastDot = className.lastIndexOf('.'); if (lastDot > 0) { className = className.substring(lastDot + 1); } final Class<? extends IObject> actualClass = graphPathBean.getClassForSimpleName(className); if (actualClass == null) { throw new IllegalArgumentException("unknown model object class named: " + className); } classes.add(actualClass); } return classes; } /** * Construct a new <q>duplicate</q> request; called from {@link GraphRequestFactory#getRequest(Class)}. * @param aclVoter ACL voter for permissions checking * @param securityRoles the security roles * @param systemTypes for identifying the system types * @param graphPathBean the graph path bean to use * @param deletionInstance a deletion instance for deleting files * @param targetClasses legal target object classes for duplicate * @param graphPolicy the graph policy to apply for duplicate * @param unnullable properties that, while nullable, may not be nulled by a graph traversal operation */ public DuplicateI(ACLVoter aclVoter, Roles securityRoles, SystemTypes systemTypes, GraphPathBean graphPathBean, Deletion deletionInstance, Set<Class<? extends IObject>> targetClasses, GraphPolicy graphPolicy, SetMultimap<String, String> unnullable) { this.aclVoter = aclVoter; this.systemTypes = systemTypes; this.graphPathBean = graphPathBean; this.targetClasses = targetClasses; this.graphPolicy = graphPolicy; this.unnullable = unnullable; } @Override public Map<String, String> getCallContext() { return null; } @Override public void init(Helper helper) { if (LOGGER.isDebugEnabled()) { final GraphUtil.ParameterReporter arguments = new GraphUtil.ParameterReporter(); arguments.addParameter("typesToDuplicate", typesToDuplicate); arguments.addParameter("typesToReference", typesToReference); arguments.addParameter("typesToIgnore", typesToIgnore); arguments.addParameter("targetObjects", targetObjects); arguments.addParameter("childOptions", childOptions); arguments.addParameter("dryRun", dryRun); LOGGER.debug("request: " + arguments); } this.helper = helper; helper.setSteps(dryRun ? 3 : 8); this.graphHelper = new GraphHelper(helper, graphPathBean); classifier = new SpecificityClassifier<Class<? extends IObject>, Inclusion>( new SpecificityClassifier.ContainmentTester<Class<? extends IObject>>() { @Override public boolean isProperSupersetOf(Class<? extends IObject> parent, Class<? extends IObject> child) { return parent != child && parent.isAssignableFrom(child); }}); try { classifier.addClass(Inclusion.DUPLICATE, getClassesFromNames(typesToDuplicate)); classifier.addClass(Inclusion.REFERENCE, getClassesFromNames(typesToReference)); classifier.addClass(Inclusion.IGNORE, getClassesFromNames(typesToIgnore)); } catch (IllegalArgumentException e) { throw helper.cancel(new ERR(), e, "bad-class"); } graphPolicyAdjusters.add(0, new Function<GraphPolicy, GraphPolicy>() { @Override public GraphPolicy apply(GraphPolicy graphPolicy) { return SkipTailPolicy.getSkipTailPolicy(graphPolicy, new Predicate<Class<? extends IObject>>() { @Override public boolean apply(Class<? extends IObject> modelObject) { final Inclusion classification = classifier.getClass(modelObject); return classification == Inclusion.REFERENCE || classification == Inclusion.IGNORE; }}); }}); graphPolicy.registerPredicate(new PermissionsPredicate()); graphTraversal = graphHelper.prepareGraphTraversal(childOptions, REQUIRED_ABILITIES, graphPolicy, graphPolicyAdjusters, aclVoter, systemTypes, graphPathBean, unnullable, new InternalProcessor(), dryRun); graphPolicyAdjusters = null; } /** * Note how to update a specific model object property. * @author m.t.b.carroll@dundee.ac.uk * @since 5.2.3 */ private static abstract class PropertyUpdate { protected final IObject duplicate; protected final String property; /** * Create a note regarding how to update a specific model object property. * @param duplicate the duplicate object holding the property * @param property the name of the property */ PropertyUpdate(IObject duplicate, String property) { this.duplicate = duplicate; this.property = property; } /** * Update the model object property to which this instance corresponds. * @param mapping the mapping from the original object's links to other objects to those objects' corresponding duplicates, * returns {@code null} if no such duplicate exists so as to avoid stealing links from the original objects * @throws GraphException if the property value could not be updated */ abstract void execute(Function<Object, IObject> mapping) throws GraphException; } /** * Note how to update a specific model object property that is directly accessible. * @author m.t.b.carroll@dundee.ac.uk * @since 5.2.3 */ private static class PropertyUpdateAccessible extends PropertyUpdate { protected final Object value; /** * Create a note regarding how to update a specific model object property. * @param duplicate the duplicate object holding the property * @param property the name of the property * @param value the property value from the original object */ PropertyUpdateAccessible(IObject duplicate, String property, Object value) { super(duplicate, property); this.value = value; } @Override void execute(Function<Object, IObject> mapping) throws GraphException { final Object duplicateValue = GraphUtil.copyComplexValue(mapping, value); if (duplicateValue != null) { try { PropertyUtils.setNestedProperty(duplicate, property, duplicateValue); } catch (NestedNullException | ReflectiveOperationException e) { throw new GraphException( "cannot set property " + property + " on duplicate " + duplicate.getClass().getName()); } } } } /** * Note how to update a specific model object property that is accessible only via {@code iterate} and {@code add} methods. * @author m.t.b.carroll@dundee.ac.uk * @since 5.2.3 */ private static class PropertyUpdateInaccessible extends PropertyUpdate { protected final IObject original; protected final Method reader, writer; protected final boolean isOrdered; private Set<IObject> written = new HashSet<IObject>(); /** * Create a note regarding how to update a specific model object property. * @param original the original object holding the property * @param duplicate the duplicate object holding the property * @param property the name of the property * @param reader the {@code iterate} method for the property * @param writer the {@code add} method for the property * @param isOrdered if the property value is a collection whose order must be preserved on duplication */ PropertyUpdateInaccessible(IObject original, IObject duplicate, String property, Method reader, Method writer, boolean isOrdered) { super(duplicate, property); this.original = original; this.reader = reader; this.writer = writer; this.isOrdered = isOrdered; } @Override void execute(Function<Object, IObject> mapping) throws GraphException { try { @SuppressWarnings("unchecked") final Iterator<IObject> linkedTos = (Iterator<IObject>) reader.invoke(original); boolean stillWriting = true; while (linkedTos.hasNext()) { final IObject linkedTo = linkedTos.next(); final IObject duplicateOfLinkedTo = mapping.apply(linkedTo); if (stillWriting && duplicateOfLinkedTo != null) { if (written.add(duplicateOfLinkedTo)) { writer.invoke(duplicate, duplicateOfLinkedTo); } } else if (isOrdered) { /* cannot easily update other than by appending so skip the rest for now */ stillWriting = false; } } } catch (NestedNullException | ReflectiveOperationException e) { throw new GraphException( "cannot set property " + property + " on duplicate " + duplicate.getClass().getName()); } } } /** * Copy simple property values to the duplicate model object. * @throws GraphException if duplication failed */ private void copySimpleProperties() throws GraphException { for (final Entry<IObject, IObject> originalAndDuplicate : originalsToDuplicates.entrySet()) { final IObject original = originalAndDuplicate.getKey(); final IObject duplicate = originalAndDuplicate.getValue(); final String originalClass = Hibernate.getClass(original).getName(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("copying properties from " + originalClass + ":" + original.getId()); } try { /* process property values for a given object that is duplicated */ for (final String superclassName : graphPathBean.getSuperclassesOfReflexive(originalClass)) { /* process property values that do not relate to edges in the model object graph */ for (final String property : graphPathBean.getSimpleProperties(superclassName)) { /* ignore inaccessible properties */ if (!graphPathBean.isPropertyAccessible(superclassName, property)) { continue; } /* copy original property value to duplicate, creating new instances of collections */ final Object value = PropertyUtils.getProperty(original, property); final Object duplicateValue = GraphUtil.copyComplexValue(Functions.constant(null), value); PropertyUtils.setProperty(duplicate, property, duplicateValue); } } } catch (NestedNullException | ReflectiveOperationException e) { throw new GraphException("failed to duplicate " + originalClass + ':' + original.getId()); } } } /** * Note in which order to persist the duplicate model objects and how to copy their properties. * @throws GraphException if duplication failed */ private void noteNewPropertyValuesForDuplicates() throws GraphException { /* allow lookup regardless of if original is actually a Hibernate proxy object */ final Function<Object, IObject> duplicateLookup = new Function<Object, IObject>() { @Override public IObject apply(Object original) { if (original instanceof IObject) { final String originalClass; if (original instanceof HibernateProxy) { originalClass = Hibernate.getClass(original).getName(); } else { originalClass = original.getClass().getName(); } final Long originalId = ((IObject) original).getId(); return originalClassIdToDuplicates.get(Maps.immutableEntry(originalClass, originalId)); } else { return null; } } }; /* note how to copy property values into duplicates and link with other model objects */ for (final Entry<IObject, IObject> originalAndDuplicate : originalsToDuplicates.entrySet()) { final IObject original = originalAndDuplicate.getKey(); final IObject duplicate = originalAndDuplicate.getValue(); final String originalClass = Hibernate.getClass(original).getName(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("noting how to copy properties from " + originalClass + ":" + original.getId()); } try { /* process property values for a given object that is duplicated */ for (final String superclassName : graphPathBean.getSuperclassesOfReflexive(originalClass)) { /* process property values that link from the duplicate to other model objects */ for (final Entry<String, String> forwardLink : graphPathBean.getLinkedTo(superclassName)) { /* next forward link */ final String linkedClassName = forwardLink.getKey(); final String property = forwardLink.getValue(); /* ignore details for now, duplicates never preserve original ownership */ if (property.startsWith("details.")) { continue; } /* note which of the objects to which the original links should be ignored */ final Set<Long> linkedToIdsToIgnore = new HashSet<Long>(); for (final Entry<String, Collection<Long>> linkedToClassIds : graphTraversal.getLinkeds(superclassName, property, original.getId()).asMap().entrySet()) { final String linkedToClass = linkedToClassIds.getKey(); final Collection<Long> linkedToIds = linkedToClassIds.getValue(); if (classifier.getClass(Class.forName(linkedToClass).asSubclass(IObject.class)) == Inclusion.IGNORE) { linkedToIdsToIgnore.addAll(linkedToIds); } } /* check for another accessor for inaccessible properties */ Object value; if (graphPathBean.isPropertyAccessible(superclassName, property)) { /* copy the linking from the original's property over to the duplicate's */ try { value = PropertyUtils.getNestedProperty(original, property); } catch (NestedNullException e) { continue; } if (value instanceof Collection) { /* if a collection property, include only the objects that aren't to be ignored */ final Collection<IObject> valueCollection = (Collection<IObject>) value; final Collection<IObject> valueToCopy; if (value instanceof List) { valueToCopy = new ArrayList<IObject>(); } else if (value instanceof Set) { valueToCopy = new HashSet<IObject>(); } else { throw new GraphException("unexpected collection type: " + value.getClass()); } for (final IObject linkedTo : valueCollection) { if (!linkedToIdsToIgnore.contains(linkedTo.getId())) { valueToCopy.add(linkedTo); } } value = valueToCopy; } else if (value instanceof IObject) { /* if the property value is to be ignored then null it */ if (linkedToIdsToIgnore.contains(((IObject) value).getId())) { value = null; } } /* note how to copy the property value, replacing originals with corresponding duplicates */ if (value != null) { propertiesToUpdate.put(duplicate, new PropertyUpdateAccessible(duplicate, property, value)); } } else { /* this could be a one-to-many property with direct accessors protected */ final Class<? extends IObject> linkerClass = Class.forName(superclassName).asSubclass(IObject.class); final Class<? extends IObject> linkedClass = Class.forName(linkedClassName).asSubclass(IObject.class); final Method reader, writer; try { reader = linkerClass.getMethod("iterate" + StringUtils.capitalize(property)); writer = linkerClass.getMethod("add" + linkedClass.getSimpleName(), linkedClass); } catch (NoSuchMethodException | SecurityException e) { /* no luck, so ignore this property */ continue; } boolean isOrdered; try { linkerClass.getMethod("getPrimary" + linkedClass.getSimpleName()); isOrdered = true; } catch (NoSuchMethodException | SecurityException e) { isOrdered = false; } value = reader.invoke(original); /* note how to copy the linking from the original's property over to the duplicate's */ propertiesToUpdate.put(duplicate, new PropertyUpdateInaccessible(original, duplicate, property, reader, writer, isOrdered)); if (isOrdered) { /* ensure that the values are written in order */ IObject previousDuplicate = null; final Iterator<IObject> valueIterator = (Iterator<IObject>) reader.invoke(original); while (valueIterator.hasNext()) { final IObject nextDuplicate = originalsToDuplicates.get(valueIterator.next()); if (nextDuplicate != null) { if (previousDuplicate != null) { blockedBy.put(nextDuplicate, previousDuplicate); } previousDuplicate = nextDuplicate; } } } } /* persist the property values before persisting the holder */ final Set<IObject> duplicatesInValue = GraphUtil.filterComplexValue(duplicateLookup, value); blockedBy.putAll(duplicate, duplicatesInValue); } } } catch (NestedNullException | ReflectiveOperationException e) { throw new GraphException("failed to duplicate " + originalClass + ':' + original.getId()); } } } /** * Duplicate model object properties, linking them as appropriate with each other and with other model objects. * @throws GraphException if duplication failed */ private void persistDuplicatesWithNewPropertyValues() throws GraphException { /* copy property values into duplicates and link with other model objects */ final Session session = helper.getSession(); final ListMultimap<IObject, IObject> propertyUpdateTriggers = ArrayListMultimap.create(); final Set<IObject> persisted = new HashSet<IObject>(); final Set<IObject> remainingTransient = new HashSet<IObject>(originalsToDuplicates.values()); while (!remainingTransient.isEmpty()) { boolean isProgress = false; final Iterator<IObject> remainingTransientIterator = remainingTransient.iterator(); while (remainingTransientIterator.hasNext()) { final IObject duplicate = remainingTransientIterator.next(); final Set<IObject> blockers = blockedBy.get(duplicate); blockers.retainAll(remainingTransient); /* changes to the view affect the underlying map */ if (blockers.isEmpty()) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("duplicating an instance of " + duplicate.getClass().getName()); } /* before persisting an object, fill in its links to other objects */ for (final PropertyUpdate update : propertiesToUpdate.get(duplicate)) { final Function<Object, IObject> duplicateProxyLookup = new Function<Object, IObject>() { @Override public IObject apply(Object original) { if (original instanceof IObject) { final String originalClass; if (original instanceof HibernateProxy) { originalClass = Hibernate.getClass(original).getName(); } else { originalClass = original.getClass().getName(); } final Long originalId = ((IObject) original).getId(); final IObject duplicate = originalClassIdToDuplicates.get(Maps.immutableEntry(originalClass, originalId)); if (duplicate == null) { return null; } if (persisted.contains(duplicate)) { return duplicate; } else { /* this value is omitted from the object's property value when it is persisted so we note * to update this property when the value is persisted */ propertyUpdateTriggers.put(duplicate, update.duplicate); } } return null; } }; update.execute(duplicateProxyLookup); } /* fill in other objects' links to the object to be persisted, such as back-references */ persisted.add(duplicate); final Function<Object, IObject> duplicateProxyLookup = new Function<Object, IObject>() { @Override public IObject apply(Object original) { if (original instanceof IObject) { final String originalClass; if (original instanceof HibernateProxy) { originalClass = Hibernate.getClass(original).getName(); } else { originalClass = original.getClass().getName(); } final Long originalId = ((IObject) original).getId(); final IObject duplicate = originalClassIdToDuplicates.get(Maps.immutableEntry(originalClass, originalId)); if (duplicate == null) { return null; } if (persisted.contains(duplicate)) { return duplicate; } } return null; } }; for (final IObject objectToUpdate : propertyUpdateTriggers.get(duplicate)) { for (final PropertyUpdate update : propertiesToUpdate.get(objectToUpdate)) { update.execute(duplicateProxyLookup); } } propertyUpdateTriggers.removeAll(duplicate); final Collection<PropertyUpdate> propertiesToUpdateForDuplicate = new ArrayList<PropertyUpdate>(propertiesToUpdate.get(duplicate)); /* when an object is persisted its hash changes, so move key in persisted and in propertiesToUpdate */ propertiesToUpdate.removeAll(duplicate); persisted.remove(duplicate); session.persist(duplicate); propertiesToUpdate.putAll(duplicate, propertiesToUpdateForDuplicate); persisted.add(duplicate); if (LOGGER.isDebugEnabled()) { LOGGER.debug("persisted " + duplicate.getClass().getName() + ":" + duplicate.getId()); } remainingTransientIterator.remove(); isProgress = true; } } if (!isProgress) { throw new GraphException("internal duplication error: cyclic model graph"); } } } /** * Link other model objects to the duplicates. * @throws GraphException if duplication failed */ private void linkToNewDuplicates() throws GraphException { /* copy property values into duplicates and link with other model objects */ final Session session = helper.getSession(); for (final IObject original : originalsToDuplicates.keySet()) { final String originalClass = Hibernate.getClass(original).getName(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("adjusting properties of " + originalClass + ":" + original.getId()); } try { /* process property values for a given object that is duplicated */ for (final String superclassName : graphPathBean.getSuperclassesOfReflexive(originalClass)) { /* process property values that link to the duplicate from other model objects */ for (final Entry<String, String> backwardLink : graphPathBean.getLinkedBy(superclassName)) { /* next backward link */ final String linkingClass = backwardLink.getKey(); final String property = backwardLink.getValue(); /* ignore inaccessible properties */ if (!graphPathBean.isPropertyAccessible(linkingClass, property)) { continue; } for (final Entry<String, Collection<Long>> linkedFromClassIds : graphTraversal.getLinkers(linkingClass, property, original.getId()).asMap().entrySet()) { final String linkedFromClass = linkedFromClassIds.getKey(); final Collection<Long> linkedFromIds = linkedFromClassIds.getValue(); if (classifier.getClass(Class.forName(linkedFromClass).asSubclass(IObject.class)) == Inclusion.IGNORE) { /* these linkers are to be ignored */ continue; } /* load the instances that link to the original */ final String rootQuery = "FROM " + linkedFromClass + " WHERE id IN (:ids)"; for (final List<Long> idsBatch : Iterables.partition(linkedFromIds, BATCH_SIZE)) { final List<IObject> linkers = session.createQuery(rootQuery).setParameterList("ids", idsBatch).list(); for (final IObject linker : linkers) { if (originalsToDuplicates.containsKey(linker)) { /* ignore linkers that are to be duplicated, those have already been handled */ continue; } /* copy the linking from the original's property over to the duplicate's */ final Object value; try { value = PropertyUtils.getNestedProperty(linker, property); } catch (NestedNullException e) { continue; } /* for linkers adjust only the collection properties */ if (value instanceof Collection) { final Collection<IObject> valueCollection = (Collection<IObject>) value; final Collection<IObject> newDuplicates = new ArrayList<IObject>(); for (final IObject originalLinker : valueCollection) { IObject duplicateOfValue = originalsToDuplicates.get(originalLinker); if (duplicateOfValue != null) { /* previous had just original, now include duplicate too */ newDuplicates.add(duplicateOfValue); } } valueCollection.addAll(newDuplicates); } } } } } } } catch (NestedNullException | ReflectiveOperationException e) { throw new GraphException("failed to adjust " + originalClass + ':' + original.getId()); } } } @Override public Object step(int step) throws Cancel { helper.assertStep(step); try { switch (step) { case 0: final SetMultimap<String, Long> targetMultimap = graphHelper.getTargetMultimap(targetClasses, targetObjects); targetObjectCount += targetMultimap.size(); final Entry<SetMultimap<String, Long>, SetMultimap<String, Long>> plan = graphTraversal.planOperation(helper.getSession(), targetMultimap, true, true); if (plan.getValue().isEmpty()) { graphTraversal.assertNoUnlinking(); } else { final Exception e = new IllegalArgumentException("duplication plan unexpectedly includes deletion"); throw helper.cancel(new ERR(), e, "bad-plan"); } return plan.getKey(); case 1: graphTraversal.assertNoPolicyViolations(); return null; case 2: processor = graphTraversal.processTargets(); return null; case 3: processor.execute(); return null; case 4: copySimpleProperties(); return null; case 5: noteNewPropertyValuesForDuplicates(); return null; case 6: persistDuplicatesWithNewPropertyValues(); return null; case 7: linkToNewDuplicates(); return null; default: final Exception e = new IllegalArgumentException("model object graph operation has no step " + step); throw helper.cancel(new ERR(), e, "bad-step"); } } catch (Cancel c) { throw c; } catch (GraphException ge) { final omero.cmd.GraphException graphERR = new omero.cmd.GraphException(); graphERR.message = ge.message; throw helper.cancel(graphERR, ge, "graph-fail"); } catch (Throwable t) { throw helper.cancel(new ERR(), t, "graph-fail"); } } @Override public void finish() { } @Override public void buildResponse(int step, Object object) { helper.assertResponse(step); /* if the results object were in terms of IObjectList then this would need IceMapper.map */ if (dryRun && step == 0) { final SetMultimap<String, Long> result = (SetMultimap<String, Long>) object; final Map<String, List<Long>> duplicatedObjects = GraphUtil.copyMultimapForResponse(result); duplicatedObjectCount += result.size(); final DuplicateResponse response = new DuplicateResponse(duplicatedObjects); helper.setResponseIfNull(response); helper.info("in mock duplication of " + targetObjectCount + ", duplicated " + duplicatedObjectCount + " in total"); } else if (!dryRun && step == 6) { final Map<String, List<Long>> duplicatedObjects = new HashMap<String, List<Long>>(); for (final IObject duplicate : originalsToDuplicates.values()) { final String className = duplicate.getClass().getName(); List<Long> ids = duplicatedObjects.get(className); if (ids == null) { ids = new ArrayList<Long>(); duplicatedObjects.put(className, ids); } ids.add(duplicate.getId()); duplicatedObjectCount++; } final DuplicateResponse response = new DuplicateResponse(duplicatedObjects); helper.setResponseIfNull(response); helper.info("in duplication of " + targetObjectCount + ", duplicated " + duplicatedObjectCount + " in total"); if (LOGGER.isDebugEnabled()) { final GraphUtil.ParameterReporter arguments = new GraphUtil.ParameterReporter(); arguments.addParameter("duplicates", response.duplicates); LOGGER.debug("response: " + arguments); } } } @Override public Response getResponse() { return helper.getResponse(); } @Override public void copyFieldsTo(Duplicate request) { GraphUtil.copyFields(this, request); request.typesToDuplicate = new ArrayList<String>(typesToDuplicate); request.typesToReference = new ArrayList<String>(typesToReference); request.typesToIgnore = new ArrayList<String>(typesToIgnore); } @Override public void adjustGraphPolicy(Function<GraphPolicy, GraphPolicy> adjuster) { if (graphPolicyAdjusters == null) { throw new IllegalStateException("request is already initialized"); } else { graphPolicyAdjusters.add(adjuster); } } @Override public int getStepProvidingCompleteResponse() { return dryRun ? 0 : 6; } @Override public GraphPolicy.Action getActionForStarting() { return GraphPolicy.Action.INCLUDE; } @Override public Map<String, List<Long>> getStartFrom(Response response) { return ((DuplicateResponse) response).duplicates; } /** * A <q>duplicate</q> processor that assists with duplicating model objects. * This processor merely reads from the database and initializes the duplicate objects. * The updates to the objects' property values occur in a later step. * @author m.t.b.carroll@dundee.ac.uk * @since 5.2.1 */ private final class InternalProcessor extends BaseGraphTraversalProcessor { public InternalProcessor() { super(helper.getSession()); } @Override public void processInstances(String className, Collection<Long> ids) throws GraphException { final String rootQuery = "FROM " + className + " WHERE id IN (:ids)"; for (final List<Long> idsBatch : Iterables.partition(ids, BATCH_SIZE)) { final List<IObject> originals = session.createQuery(rootQuery).setParameterList("ids", idsBatch).list(); for (final IObject original : originals) { final IObject duplicate; try { duplicate = (IObject) Hibernate.getClass(original).newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new GraphException("cannot create a duplicate of " + original); } final String originalClass = Hibernate.getClass(original).getName(); final Long originalId = original.getId(); originalClassIdToDuplicates.put(Maps.immutableEntry(originalClass, originalId), duplicate); originalsToDuplicates.put(original, duplicate); } } } @Override public Set<GraphPolicy.Ability> getRequiredPermissions() { return REQUIRED_ABILITIES; } } }