/*
* 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.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.SetMultimap;
import ome.model.IObject;
import ome.model.internal.Permissions;
import ome.model.meta.ExperimenterGroup;
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.system.EventContext;
import ome.system.Login;
import ome.system.Roles;
import omero.cmd.Chgrp2;
import omero.cmd.Chgrp2Response;
import omero.cmd.HandleI.Cancel;
import omero.cmd.ERR;
import omero.cmd.Helper;
import omero.cmd.IRequest;
import omero.cmd.Response;
/**
* Request to move model objects to a different experiment group, replacing version 5.0's {@code ChgrpI}.
* @author m.t.b.carroll@dundee.ac.uk
* @since 5.1.0
*/
public class Chgrp2I extends Chgrp2 implements IRequest, WrappableRequest<Chgrp2> {
private static final Logger LOGGER = LoggerFactory.getLogger(Chgrp2I.class);
private static final ImmutableMap<String, String> ALL_GROUPS_CONTEXT = ImmutableMap.of(Login.OMERO_GROUP, "-1");
private static final Set<GraphPolicy.Ability> REQUIRED_ABILITIES = ImmutableSet.of(GraphPolicy.Ability.OWN);
private final ACLVoter aclVoter;
private final SystemTypes systemTypes;
private final GraphPathBean graphPathBean;
private final Deletion deletionInstance;
private final Set<Class<? extends IObject>> targetClasses;
private GraphPolicy graphPolicy; /* not final because of adjustGraphPolicy */
private final SetMultimap<String, String> unnullable;
private List<Function<GraphPolicy, GraphPolicy>> graphPolicyAdjusters = new ArrayList<Function<GraphPolicy, GraphPolicy>>();
private Helper helper;
private GraphHelper graphHelper;
private GraphTraversal graphTraversal;
private GraphTraversal.PlanExecutor unlinker;
private GraphTraversal.PlanExecutor processor;
private int targetObjectCount = 0;
private int deletedObjectCount = 0;
private int movedObjectCount = 0;
/**
* Construct a new <q>chgrp</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 chgrp
* @param graphPolicy the graph policy to apply for chgrp
* @param unnullable properties that, while nullable, may not be nulled by a graph traversal operation
*/
public Chgrp2I(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.deletionInstance = deletionInstance;
this.targetClasses = targetClasses;
this.graphPolicy = graphPolicy;
this.unnullable = unnullable;
}
@Override
public Map<String, String> getCallContext() {
return new HashMap<String, String>(ALL_GROUPS_CONTEXT);
}
@Override
public void init(Helper helper) {
if (LOGGER.isDebugEnabled()) {
final GraphUtil.ParameterReporter arguments = new GraphUtil.ParameterReporter();
arguments.addParameter("groupId", groupId);
arguments.addParameter("targetObjects", targetObjects);
arguments.addParameter("childOptions", childOptions);
arguments.addParameter("dryRun", dryRun);
LOGGER.debug("request: " + arguments);
}
this.helper = helper;
helper.setSteps(dryRun ? 4 : 6);
this.graphHelper = new GraphHelper(helper, graphPathBean);
/* check that the user is a member of the destination group */
final EventContext eventContext = helper.getEventContext();
if (!(eventContext.isCurrentUserAdmin() || eventContext.getMemberOfGroupsList().contains(groupId))) {
final Exception e = new IllegalArgumentException("not a member of the chgrp destination group");
throw helper.cancel(new ERR(), e, "not-in-group");
}
final ExperimenterGroup destinationGroup = (ExperimenterGroup) helper.getSession().get(ExperimenterGroup.class, groupId);
final Permissions destinationGroupPermissions = destinationGroup.getDetails().getPermissions();
final boolean isToGroupReadable = destinationGroupPermissions.isGranted(Permissions.Role.GROUP, Permissions.Right.READ);
if (!isToGroupReadable) {
graphPolicy.setCondition("to_private");
}
graphTraversal = graphHelper.prepareGraphTraversal(childOptions, REQUIRED_ABILITIES, graphPolicy, graphPolicyAdjusters,
aclVoter, systemTypes, graphPathBean, unnullable, new InternalProcessor(), dryRun);
graphPolicyAdjusters = null;
}
@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);
return Maps.immutableEntry(plan.getKey(), GraphUtil.arrangeDeletionTargets(helper.getSession(), plan.getValue()));
case 1:
graphTraversal.assertNoPolicyViolations();
return null;
case 2:
processor = graphTraversal.processTargets();
return null;
case 3:
unlinker = graphTraversal.unlinkTargets(true);
graphTraversal = null;
return null;
case 4:
unlinker.execute();
return null;
case 5:
processor.execute();
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 (step == 0) {
/* if the results object were in terms of IObjectList then this would need IceMapper.map */
final Entry<SetMultimap<String, Long>, SetMultimap<String, Long>> result =
(Entry<SetMultimap<String, Long>, SetMultimap<String, Long>>) object;
if (!dryRun) {
try {
deletionInstance.deleteFiles(GraphUtil.trimPackageNames(result.getValue()));
} catch (Exception e) {
helper.cancel(new ERR(), e, "file-delete-fail");
}
}
final Map<String, List<Long>> movedObjects = GraphUtil.copyMultimapForResponse(result.getKey());
final Map<String, List<Long>> deletedObjects = GraphUtil.copyMultimapForResponse(result.getValue());
movedObjectCount += result.getKey().size();
deletedObjectCount += result.getValue().size();
final Chgrp2Response response = new Chgrp2Response(movedObjects, deletedObjects);
helper.setResponseIfNull(response);
helper.info("in " + (dryRun ? "mock " : "") + "chgrp to " + groupId + " of " + targetObjectCount +
", moved " + movedObjectCount + " and deleted " + deletedObjectCount + " in total");
if (LOGGER.isDebugEnabled()) {
final GraphUtil.ParameterReporter arguments = new GraphUtil.ParameterReporter();
arguments.addParameter("includedObjects", response.includedObjects);
arguments.addParameter("deletedObjects", response.deletedObjects);
LOGGER.debug("response: " + arguments);
}
}
}
@Override
public Response getResponse() {
return helper.getResponse();
}
@Override
public void copyFieldsTo(Chgrp2 request) {
GraphUtil.copyFields(this, request);
request.groupId = groupId;
}
@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 0;
}
@Override
public GraphPolicy.Action getActionForStarting() {
return GraphPolicy.Action.INCLUDE;
}
@Override
public Map<String, List<Long>> getStartFrom(Response response) {
return ((Chgrp2Response) response).includedObjects;
}
/**
* A <q>chgrp</q> processor that updates model objects' group.
* @author m.t.b.carroll@dundee.ac.uk
* @since 5.1.0
*/
private final class InternalProcessor extends BaseGraphTraversalProcessor {
private final Logger LOGGER = LoggerFactory.getLogger(InternalProcessor.class);
private final ExperimenterGroup group = new ExperimenterGroup(groupId, false);
public InternalProcessor() {
super(helper.getSession());
}
@Override
public void processInstances(String className, Collection<Long> ids) throws GraphException {
final String update = "UPDATE " + className + " SET details.group = :group WHERE id IN (:ids)";
final int count =
session.createQuery(update).setParameter("group", group).setParameterList("ids", ids).executeUpdate();
if (count != ids.size()) {
LOGGER.warn("not all the objects of type " + className + " could be processed");
}
}
@Override
public Set<GraphPolicy.Ability> getRequiredPermissions() {
return REQUIRED_ABILITIES;
}
}
}