/* * Copyright (c) 2010-2015 Evolveum * * 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 com.evolveum.midpoint.certification.impl; import com.evolveum.midpoint.certification.impl.handlers.CertificationHandler; import com.evolveum.midpoint.common.Clock; import com.evolveum.midpoint.model.api.ModelExecuteOptions; import com.evolveum.midpoint.model.api.ModelService; import com.evolveum.midpoint.model.common.expression.ExpressionVariables; import com.evolveum.midpoint.prism.PrismContainerValue; import com.evolveum.midpoint.prism.PrismContext; import com.evolveum.midpoint.prism.PrismObject; import com.evolveum.midpoint.prism.PrismReferenceValue; import com.evolveum.midpoint.prism.delta.ContainerDelta; import com.evolveum.midpoint.prism.delta.ItemDelta; import com.evolveum.midpoint.prism.delta.ObjectDelta; import com.evolveum.midpoint.prism.delta.PropertyDelta; import com.evolveum.midpoint.prism.delta.builder.DeltaBuilder; import com.evolveum.midpoint.prism.marshaller.QueryConvertor; import com.evolveum.midpoint.prism.polystring.PolyString; import com.evolveum.midpoint.prism.query.ObjectFilter; import com.evolveum.midpoint.prism.query.ObjectQuery; import com.evolveum.midpoint.prism.query.builder.QueryBuilder; import com.evolveum.midpoint.prism.util.CloneUtil; import com.evolveum.midpoint.prism.util.PrismUtil; import com.evolveum.midpoint.prism.xml.XmlTypeConverter; import com.evolveum.midpoint.repo.api.RepositoryService; import com.evolveum.midpoint.schema.ObjectDeltaOperation; import com.evolveum.midpoint.schema.SearchResultList; import com.evolveum.midpoint.schema.constants.ExpressionConstants; import com.evolveum.midpoint.schema.constants.ObjectTypes; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.schema.util.CertCampaignTypeUtil; import com.evolveum.midpoint.schema.util.ObjectQueryUtil; import com.evolveum.midpoint.schema.util.ObjectTypeUtil; import com.evolveum.midpoint.schema.util.WfContextUtil; import com.evolveum.midpoint.security.api.MidPointPrincipal; import com.evolveum.midpoint.security.api.SecurityEnforcer; import com.evolveum.midpoint.task.api.Task; import com.evolveum.midpoint.util.DebugUtil; import com.evolveum.midpoint.util.exception.CommunicationException; import com.evolveum.midpoint.util.exception.ConfigurationException; import com.evolveum.midpoint.util.exception.ExpressionEvaluationException; import com.evolveum.midpoint.util.exception.ObjectAlreadyExistsException; import com.evolveum.midpoint.util.exception.ObjectNotFoundException; import com.evolveum.midpoint.util.exception.PolicyViolationException; import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.midpoint.util.exception.SecurityViolationException; import com.evolveum.midpoint.util.exception.SystemException; import com.evolveum.midpoint.util.logging.LoggingUtils; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import com.evolveum.midpoint.xml.ns._public.common.common_3.*; import com.evolveum.prism.xml.ns._public.types_3.PolyStringType; import org.apache.commons.lang3.Validate; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import javax.xml.datatype.Duration; import javax.xml.datatype.XMLGregorianCalendar; import java.util.*; import static com.evolveum.midpoint.schema.util.ObjectTypeUtil.toShortString; import static com.evolveum.midpoint.xml.ns._public.common.common_3.AbstractWorkItemType.F_ASSIGNEE_REF; import static com.evolveum.midpoint.xml.ns._public.common.common_3.AbstractWorkItemType.F_CLOSE_TIMESTAMP; import static com.evolveum.midpoint.xml.ns._public.common.common_3.AbstractWorkItemType.F_ESCALATION_LEVEL; import static com.evolveum.midpoint.xml.ns._public.common.common_3.AccessCertificationCampaignStateType.*; import static com.evolveum.midpoint.xml.ns._public.common.common_3.AccessCertificationCampaignType.F_CASE; import static com.evolveum.midpoint.xml.ns._public.common.common_3.AccessCertificationCampaignType.F_STAGE; import static com.evolveum.midpoint.xml.ns._public.common.common_3.AccessCertificationCaseType.F_EVENT; import static com.evolveum.midpoint.xml.ns._public.common.common_3.AccessCertificationCaseType.F_WORK_ITEM; import static com.evolveum.midpoint.xml.ns._public.common.common_3.AccessCertificationDefinitionType.F_LAST_CAMPAIGN_CLOSED_TIMESTAMP; import static com.evolveum.midpoint.xml.ns._public.common.common_3.AccessCertificationDefinitionType.F_LAST_CAMPAIGN_ID_USED; import static com.evolveum.midpoint.xml.ns._public.common.common_3.AccessCertificationDefinitionType.F_LAST_CAMPAIGN_STARTED_TIMESTAMP; import static com.evolveum.midpoint.xml.ns._public.common.common_3.AccessCertificationStageType.F_END_TIMESTAMP; /** * @author mederly */ @Component public class AccCertUpdateHelper { private static final transient Trace LOGGER = TraceManager.getTrace(AccCertUpdateHelper.class); @Autowired private AccCertEventHelper eventHelper; @Autowired private PrismContext prismContext; @Autowired private ModelService modelService; @Autowired @Qualifier("cacheRepositoryService") private RepositoryService repositoryService; @Autowired private SecurityEnforcer securityEnforcer; @Autowired private AccCertGeneralHelper generalHelper; @Autowired protected AccCertQueryHelper queryHelper; @Autowired private AccCertCaseOperationsHelper caseHelper; @Autowired private Clock clock; @Autowired private AccCertExpressionHelper expressionHelper; //region ================================ Campaign create ================================ AccessCertificationCampaignType createCampaignObject(AccessCertificationDefinitionType definition, Task task, OperationResult result) throws SchemaException, ObjectNotFoundException, SecurityViolationException { AccessCertificationCampaignType newCampaign = new AccessCertificationCampaignType(prismContext); if (definition.getName() != null) { newCampaign.setName(generateCampaignName(definition, task, result)); } else { throw new SchemaException("Couldn't create a campaign without name"); } newCampaign.setDescription(definition.getDescription()); newCampaign.setOwnerRef(securityEnforcer.getPrincipal().toObjectReference()); newCampaign.setTenantRef(definition.getTenantRef()); newCampaign.setDefinitionRef(ObjectTypeUtil.createObjectRef(definition)); if (definition.getHandlerUri() != null) { newCampaign.setHandlerUri(definition.getHandlerUri()); } else { throw new SchemaException("Couldn't create a campaign without handlerUri"); } newCampaign.setScopeDefinition(definition.getScopeDefinition()); newCampaign.setRemediationDefinition(definition.getRemediationDefinition()); newCampaign.getStageDefinition().addAll(CloneUtil.cloneCollectionMembers(definition.getStageDefinition())); CertCampaignTypeUtil.checkStageDefinitionConsistency(newCampaign.getStageDefinition()); newCampaign.setReviewStrategy(definition.getReviewStrategy()); newCampaign.setStartTimestamp(null); newCampaign.setEndTimestamp(null); newCampaign.setState(CREATED); newCampaign.setStageNumber(0); return newCampaign; } <O extends ObjectType> AccessCertificationCampaignType createAdHocCampaignObject( AccessCertificationDefinitionType definition, PrismObject<O> focus, Task task, OperationResult result) throws SecurityViolationException, ObjectNotFoundException, SchemaException { definition.setName(PolyStringType.fromOrig(PolyString.getOrig(definition.getName()) + " " + PolyString.getOrig(focus.getName()))); definition.setLastCampaignIdUsed(null); AccessCertificationCampaignType campaign = createCampaignObject(definition, task, result); AccessCertificationObjectBasedScopeType scope; if ((campaign.getScopeDefinition() instanceof AccessCertificationObjectBasedScopeType)) { scope = (AccessCertificationObjectBasedScopeType) campaign.getScopeDefinition(); } else { // TODO! scope = new AccessCertificationAssignmentReviewScopeType(prismContext); campaign.setScopeDefinition(scope); } Class<? extends ObjectType> focusClass = focus.asObjectable().getClass(); scope.setObjectType(ObjectTypes.getObjectType(focusClass).getTypeQName()); ObjectFilter objectFilter = QueryBuilder.queryFor(focusClass, prismContext).id(focus.getOid()).buildFilter(); scope.setSearchFilter(QueryConvertor.createSearchFilterType(objectFilter, prismContext)); return campaign; } private PolyStringType generateCampaignName(AccessCertificationDefinitionType definition, Task task, OperationResult result) throws SchemaException, ObjectNotFoundException { String prefix = definition.getName().getOrig(); Integer lastCampaignIdUsed = definition.getLastCampaignIdUsed() != null ? definition.getLastCampaignIdUsed() : 0; for (int i = lastCampaignIdUsed+1;; i++) { String name = generateName(prefix, i); if (!campaignExists(name, result)) { recordLastCampaignIdUsed(definition.getOid(), i, task, result); return new PolyStringType(name); } } } private boolean campaignExists(String name, OperationResult result) throws SchemaException { ObjectQuery query = ObjectQueryUtil.createNameQuery(AccessCertificationCampaignType.class, prismContext, name); SearchResultList<PrismObject<AccessCertificationCampaignType>> existingCampaigns = repositoryService.searchObjects(AccessCertificationCampaignType.class, query, null, result); return !existingCampaigns.isEmpty(); } private String generateName(String prefix, int i) { return prefix + " " + i; } public void recordLastCampaignIdUsed(String definitionOid, int lastIdUsed, Task task, OperationResult result) { try { List<ItemDelta<?,?>> modifications = DeltaBuilder.deltaFor(AccessCertificationDefinitionType.class, prismContext) .item(F_LAST_CAMPAIGN_ID_USED).replace(lastIdUsed) .asItemDeltas(); modifyObjectViaModel(AccessCertificationDefinitionType.class, definitionOid, modifications, task, result); } catch (SchemaException|ObjectNotFoundException|RuntimeException|ObjectAlreadyExistsException e) { LoggingUtils.logUnexpectedException(LOGGER, "Couldn't update last campaign ID for definition {}", e, definitionOid); } } //endregion //region ================================ Stage open ================================ public List<ItemDelta<?,?>> getDeltasForStageOpen(AccessCertificationCampaignType campaign, AccessCertificationStageType stage, CertificationHandler handler, final Task task, OperationResult result) throws SchemaException, ObjectNotFoundException, ObjectAlreadyExistsException { Validate.notNull(campaign, "certificationCampaign"); Validate.notNull(campaign.getOid(), "certificationCampaign.oid"); int stageNumber = campaign.getStageNumber(); int newStageNumber = stageNumber + 1; if (LOGGER.isTraceEnabled()) { LOGGER.trace("getDeltasForStageOpen starting; campaign = {}, stage number = {}", ObjectTypeUtil.toShortString(campaign), stageNumber); } List<ItemDelta<?,?>> rv = new ArrayList<>(); if (stageNumber == 0) { rv.addAll(caseHelper.getDeltasToCreateCases(campaign, stage, handler, task, result)); } else { rv.addAll(caseHelper.getDeltasToAdvanceCases(campaign, stage, task, result)); } rv.add(createStageAddDelta(stage)); rv.addAll(createDeltasToRecordStageOpen(campaign, stage)); rv.addAll(createTriggersForTimedActions(campaign.getOid(), 0, XmlTypeConverter.toDate(stage.getStartTimestamp()), XmlTypeConverter.toDate(stage.getDeadline()), CertCampaignTypeUtil.findStageDefinition(campaign, newStageNumber).getTimedActions())); LOGGER.trace("getDeltasForStageOpen finishing, returning {} deltas:\n{}", rv.size(), DebugUtil.debugDumpLazily(rv)); return rv; } // some bureaucracy... stage#, state, start time, triggers List<ItemDelta<?,?>> createDeltasToRecordStageOpen(AccessCertificationCampaignType campaign, AccessCertificationStageType newStage) throws ObjectNotFoundException, SchemaException, ObjectAlreadyExistsException { final List<ItemDelta<?,?>> itemDeltaList = new ArrayList<>(); itemDeltaList.add(createStageNumberDelta(newStage.getNumber())); final PropertyDelta<AccessCertificationCampaignStateType> stateDelta = createStateDelta(IN_REVIEW_STAGE); itemDeltaList.add(stateDelta); final boolean campaignJustCreated = newStage.getNumber() == 1; if (campaignJustCreated) { PropertyDelta<XMLGregorianCalendar> startDelta = createStartTimeDelta(XmlTypeConverter.createXMLGregorianCalendar(new Date())); itemDeltaList.add(startDelta); } final XMLGregorianCalendar stageDeadline = newStage.getDeadline(); if (stageDeadline != null) { // auto-closing and notifications triggers final AccessCertificationStageDefinitionType stageDef = CertCampaignTypeUtil.findStageDefinition(campaign, newStage.getNumber()); List<TriggerType> triggers = new ArrayList<>(); // pseudo-random ID so this trigger will not be deleted by trigger task handler (if this code itself is executed as part of previous trigger firing) // TODO implement this more seriously! long lastId = (long) (Math.random() * 1000000000); final TriggerType triggerClose = new TriggerType(prismContext); triggerClose.setHandlerUri(AccessCertificationCloseStageTriggerHandler.HANDLER_URI); triggerClose.setTimestamp(stageDeadline); triggerClose.setId(lastId); triggers.add(triggerClose); for (Duration beforeDeadline : stageDef.getNotifyBeforeDeadline()) { final XMLGregorianCalendar beforeEnd = CloneUtil.clone(stageDeadline); beforeEnd.add(beforeDeadline.negate()); if (XmlTypeConverter.toMillis(beforeEnd) > System.currentTimeMillis()) { final TriggerType triggerBeforeEnd = new TriggerType(prismContext); triggerBeforeEnd.setHandlerUri(AccessCertificationCloseStageApproachingTriggerHandler.HANDLER_URI); triggerBeforeEnd.setTimestamp(beforeEnd); triggerBeforeEnd.setId(++lastId); triggers.add(triggerBeforeEnd); } } ContainerDelta<TriggerType> triggerDelta = ContainerDelta.createModificationReplace(ObjectType.F_TRIGGER, AccessCertificationCampaignType.class, prismContext, triggers); itemDeltaList.add(triggerDelta); } return itemDeltaList; } protected AccessCertificationStageType createStage(AccessCertificationCampaignType campaign, int requestedStageNumber) { AccessCertificationStageType stage = new AccessCertificationStageType(prismContext); stage.setNumber(requestedStageNumber); stage.setStartTimestamp(XmlTypeConverter.createXMLGregorianCalendar(new Date())); AccessCertificationStageDefinitionType stageDef = CertCampaignTypeUtil.findStageDefinition(campaign, stage.getNumber()); XMLGregorianCalendar deadline = computeDeadline(stage.getStartTimestamp(), stageDef.getDuration(), stageDef.getDeadlineRounding()); stage.setDeadline(deadline); stage.setName(stageDef.getName()); stage.setDescription(stageDef.getDescription()); return stage; } private XMLGregorianCalendar computeDeadline(XMLGregorianCalendar start, Duration duration, DeadlineRoundingType deadlineRounding) { XMLGregorianCalendar deadline = (XMLGregorianCalendar) start.clone(); if (duration != null) { deadline.add(duration); } DeadlineRoundingType rounding = deadlineRounding != null ? deadlineRounding : DeadlineRoundingType.DAY; switch (rounding) { case DAY: deadline.setHour(23); case HOUR: deadline.setMinute(59); deadline.setSecond(59); deadline.setMillisecond(999); case NONE: // nothing here } return deadline; } void afterStageOpen(String campaignOid, AccessCertificationStageType newStage, Task task, OperationResult result) throws SchemaException, ObjectNotFoundException, ObjectAlreadyExistsException { // notifications final AccessCertificationCampaignType campaign = generalHelper.getCampaign(campaignOid, null, task, result); if (campaign.getStageNumber() == 1) { eventHelper.onCampaignStart(campaign, task, result); } eventHelper.onCampaignStageStart(campaign, task, result); notifyReviewers(campaign, false, task, result); if (newStage.getNumber() == 1 && campaign.getDefinitionRef() != null) { List<ItemDelta<?,?>> deltas = DeltaBuilder.deltaFor(AccessCertificationDefinitionType.class, prismContext) .item(F_LAST_CAMPAIGN_STARTED_TIMESTAMP).replace(XmlTypeConverter.createXMLGregorianCalendar(new Date())) .asItemDeltas(); modifyObjectViaModel(AccessCertificationDefinitionType.class, campaign.getDefinitionRef().getOid(), deltas, task, result); } } private void notifyReviewers(AccessCertificationCampaignType campaign, boolean unansweredOnly, Task task, OperationResult result) throws SchemaException { final List<AccessCertificationCaseType> caseList = queryHelper.searchCases(campaign.getOid(), null, null, result); Collection<String> reviewers = eventHelper.getCurrentActiveReviewers(caseList); for (String reviewerOid : reviewers) { List<AccessCertificationCaseType> cases = queryHelper.getCasesForReviewer(campaign, reviewerOid, task, result); boolean notify = !unansweredOnly || cases.stream() .flatMap(c -> c.getWorkItem().stream()) .anyMatch(wi -> ObjectTypeUtil.containsOid(wi.getAssigneeRef(), reviewerOid) && (wi.getOutput() == null || wi.getOutput().getOutcome() == null)); if (notify) { ObjectReferenceType reviewerRef = ObjectTypeUtil.createObjectRef(reviewerOid, ObjectTypes.USER); eventHelper.onReviewRequested(reviewerRef, cases, campaign, task, result); } } } //endregion //region ================================ Delegation/escalation ================================ public void delegateWorkItems(String campaignOid, List<AccessCertificationWorkItemType> workItems, DelegateWorkItemActionType delegateAction, Task task, OperationResult result) throws SchemaException, ObjectNotFoundException, ObjectAlreadyExistsException, ExpressionEvaluationException, SecurityViolationException { LOGGER.info("Going to delegate {} work item(s) in campaign {}", workItems.size(), campaignOid); MidPointPrincipal principal = securityEnforcer.getPrincipal(); result.addContext("user", toShortString(principal.getUser())); ObjectReferenceType initiator = ObjectTypeUtil.createObjectRef(principal.getUser()); XMLGregorianCalendar now = clock.currentTimeXMLGregorianCalendar(); List<ItemDelta<?, ?>> deltas = new ArrayList<>(); for (AccessCertificationWorkItemType workItem : workItems) { AccessCertificationCaseType aCase = CertCampaignTypeUtil.getCaseChecked(workItem); AccessCertificationCampaignType campaign = CertCampaignTypeUtil.getCampaignChecked(aCase); if (!java.util.Objects.equals(campaign.getOid(), campaignOid)) { throw new IllegalArgumentException("Work item to delegate does not belong to specified campaign (" + campaignOid + ") but to " + campaign); } // TODO reload the work item here (and replace exceptions with logged warnings) if (workItem.getCloseTimestamp() != null) { throw new IllegalStateException("Couldn't delegate a work item that is already closed: " + workItem); } if (workItem.getStageNumber() != campaign.getStageNumber()) { throw new IllegalStateException("Couldn't delegate a work item that is not in a current stage. Current stage: " + campaign.getStageNumber() + ", work item stage: " + workItem.getStageNumber()); } List<ObjectReferenceType> delegates = computeDelegateTo(delegateAction, workItem, aCase, campaign, task, result); WorkItemEventCauseInformationType causeInformation = null; // TODO LOGGER.trace("Delegating work item {} to {}: cause={}", workItem, delegates, causeInformation); List<ObjectReferenceType> assigneesBefore = CloneUtil.cloneCollectionMembers(workItem.getAssigneeRef()); WorkItemDelegationMethodType method = getDelegationMethod(delegateAction); List<ObjectReferenceType> newAssignees = new ArrayList<>(); List<ObjectReferenceType> delegatedTo = new ArrayList<>(); WfContextUtil.computeAssignees(newAssignees, delegatedTo, delegates, method, workItem); WorkItemDelegationEventType event = WfContextUtil.createDelegationEvent(null, assigneesBefore, delegatedTo, method, causeInformation); event.setTimestamp(now); event.setInitiatorRef(initiator); event.setWorkItemId(workItem.getId()); event.setEscalationLevel(workItem.getEscalationLevel()); addDeltasForAssigneesAndEvent(deltas, workItem, aCase, newAssignees, event); // notification (after modifications) } modifyObjectViaModel(AccessCertificationCampaignType.class, campaignOid, deltas, task, result); // TODO notifications // AccessCertificationCampaignType updatedCampaign = refreshCampaign(campaign, task, result); // LOGGER.info("Updated campaign state: {}", updatedCampaign.getState()); // eventHelper.onCampaignEnd(updatedCampaign, task, result); } @NotNull private WorkItemDelegationMethodType getDelegationMethod(DelegateWorkItemActionType delegateAction) { WorkItemDelegationMethodType method = delegateAction.getDelegationMethod(); if (method == null) { method = WorkItemDelegationMethodType.REPLACE_ASSIGNEES; } return method; } public void escalateCampaign(String campaignOid, EscalateWorkItemActionType escalateAction, WorkItemEventCauseInformationType causeInformation, Task task, OperationResult result) throws SchemaException, ObjectNotFoundException, ObjectAlreadyExistsException, ExpressionEvaluationException, SecurityViolationException { MidPointPrincipal principal = securityEnforcer.getPrincipal(); result.addContext("user", toShortString(principal.getUser())); ObjectReferenceType initiator = ObjectTypeUtil.createObjectRef(principal.getUser()); List<AccessCertificationWorkItemType> workItems = queryHelper.searchOpenWorkItems( CertCampaignTypeUtil.createWorkItemsForCampaignQuery(campaignOid, prismContext), null, false, null, result); if (workItems.isEmpty()) { LOGGER.debug("No work items, no escalation (campaign: {})", campaignOid); return; } LOGGER.info("Going to escalate the campaign {}: {} work item(s)", campaignOid, workItems.size()); XMLGregorianCalendar now = clock.currentTimeXMLGregorianCalendar(); List<ItemDelta<?, ?>> deltas = new ArrayList<>(); // Currently we expect all open certification work items for a given campaign to have the same escalation level. // Because of consistence with other parts of midPoint we store the escalation level within work item itself. // But we enforce it to be the same for all the open work items. // This behavior will most probably change in the future. AccessCertificationCampaignType campaign = generalHelper.getCampaign(campaignOid, null, task, result); int newStageEscalationLevelNumber = CertCampaignTypeUtil.getCurrentStageEscalationLevelNumber(campaign) + 1; WorkItemEscalationLevelType newEscalationLevel = new WorkItemEscalationLevelType() .number(newStageEscalationLevelNumber) .name(escalateAction.getEscalationLevelName()) .displayName(escalateAction.getEscalationLevelDisplayName()); for (AccessCertificationWorkItemType workItem : workItems) { AccessCertificationCaseType aCase = CertCampaignTypeUtil.getCaseChecked(workItem); AccessCertificationCampaignType workItemCampaign = CertCampaignTypeUtil.getCampaignChecked(aCase); if (!java.util.Objects.equals(workItemCampaign.getOid(), campaignOid)) { throw new IllegalArgumentException("Work item to delegate does not belong to specified campaign (" + campaignOid + ") but to " + workItemCampaign); } if (workItem.getCloseTimestamp() != null) { throw new IllegalStateException("Couldn't delegate a work item that is already closed: " + workItem); } if (workItem.getStageNumber() != workItemCampaign.getStageNumber()) { throw new IllegalStateException("Couldn't delegate a work item that is not in a current stage. Current stage: " + workItemCampaign.getStageNumber() + ", work item stage: " + workItem.getStageNumber()); } if (workItem.getOutput() != null && workItem.getOutput().getOutcome() != null) { // It is a bit questionable to skip this work item (as it is not signed off), // but it is also not quite OK to escalate it, as there's some output present. // The latter is less awkward, so let's do it that way. continue; } List<ObjectReferenceType> delegates = computeDelegateTo(escalateAction, workItem, aCase, workItemCampaign, task, result); int escalationLevel = WfContextUtil.getEscalationLevelNumber(workItem); if (escalationLevel + 1 != newStageEscalationLevelNumber) { throw new IllegalStateException("Different escalation level numbers for certification cases: work item level (" + newEscalationLevel + ") is different from the stage level (" + newStageEscalationLevelNumber + ")"); } LOGGER.debug("Escalating work item {} to level: {}; delegates={}: cause={}", workItem, newEscalationLevel, delegates, causeInformation); List<ObjectReferenceType> assigneesBefore = CloneUtil.cloneCollectionMembers(workItem.getAssigneeRef()); WorkItemDelegationMethodType method = getDelegationMethod(escalateAction); List<ObjectReferenceType> newAssignees = new ArrayList<>(); List<ObjectReferenceType> delegatedTo = new ArrayList<>(); WfContextUtil.computeAssignees(newAssignees, delegatedTo, delegates, method, workItem); WorkItemDelegationEventType event = WfContextUtil.createDelegationEvent(newEscalationLevel, assigneesBefore, delegatedTo, method, causeInformation); event.setTimestamp(now); event.setInitiatorRef(initiator); event.setWorkItemId(workItem.getId()); event.setEscalationLevel(workItem.getEscalationLevel()); addDeltasForAssigneesAndEvent(deltas, workItem, aCase, newAssignees, event); deltas.add(DeltaBuilder.deltaFor(AccessCertificationCampaignType.class, prismContext) .item(F_CASE, aCase.getId(), F_WORK_ITEM, workItem.getId(), F_ESCALATION_LEVEL).replace(newEscalationLevel) .asItemDelta()); // notification (after modifications) } AccessCertificationStageType stage = CertCampaignTypeUtil.getCurrentStage(campaign); assert stage != null; Long stageId = stage.asPrismContainerValue().getId(); assert stageId != null; deltas.add(DeltaBuilder.deltaFor(AccessCertificationCampaignType.class, prismContext) .item(F_STAGE, stageId, AccessCertificationStageType.F_ESCALATION_LEVEL).replace(newEscalationLevel) .asItemDelta()); AccessCertificationStageDefinitionType stageDefinition = CertCampaignTypeUtil.getCurrentStageDefinition(campaign); deltas.addAll(createTriggersForTimedActions(campaignOid, newStageEscalationLevelNumber, XmlTypeConverter.toDate(stage.getStartTimestamp()), XmlTypeConverter.toDate(stage.getDeadline()), stageDefinition.getTimedActions())); modifyObjectViaModel(AccessCertificationCampaignType.class, campaignOid, deltas, task, result); campaign = generalHelper.getCampaign(campaignOid, null, task, result); // TODO differentiate between "old" and "new" reviewers notifyReviewers(campaign, true, task, result); // AccessCertificationCampaignType updatedCampaign = refreshCampaign(campaign, task, result); // LOGGER.info("Updated campaign state: {}", updatedCampaign.getState()); // eventHelper.onCampaignEnd(updatedCampaign, task, result); } private void addDeltasForAssigneesAndEvent(List<ItemDelta<?, ?>> deltas, AccessCertificationWorkItemType workItem, AccessCertificationCaseType aCase, List<ObjectReferenceType> newAssignees, WorkItemDelegationEventType event) throws SchemaException { deltas.add(DeltaBuilder.deltaFor(AccessCertificationCampaignType.class, prismContext) .item(F_CASE, aCase.getId(), F_WORK_ITEM, workItem.getId(), F_ASSIGNEE_REF) .replace(PrismReferenceValue.asReferenceValues(newAssignees)) .asItemDelta()); deltas.add(DeltaBuilder.deltaFor(AccessCertificationCampaignType.class, prismContext) .item(F_CASE, aCase.getId(), F_EVENT).add(event) .asItemDelta()); } private List<ObjectReferenceType> computeDelegateTo(DelegateWorkItemActionType delegateAction, AccessCertificationWorkItemType workItem, AccessCertificationCaseType aCase, AccessCertificationCampaignType campaign, Task task, OperationResult result) throws SchemaException, ObjectNotFoundException, ExpressionEvaluationException { List<ObjectReferenceType> rv = new ArrayList<>(); rv.addAll(CloneUtil.cloneCollectionMembers(delegateAction.getApproverRef())); if (!delegateAction.getApproverExpression().isEmpty()) { ExpressionVariables variables = new ExpressionVariables(); variables.addVariableDefinition(ExpressionConstants.VAR_WORK_ITEM, workItem); variables.addVariableDefinition(ExpressionConstants.VAR_CERTIFICATION_CASE, aCase); variables.addVariableDefinition(ExpressionConstants.VAR_CAMPAIGN, campaign); for (ExpressionType expression : delegateAction.getApproverExpression()) { rv.addAll(expressionHelper.evaluateRefExpressionChecked(expression, variables, "computing delegates", task, result)); } } return rv; } //endregion //region ================================ Triggers ================================ // see also MidpointUtil.createTriggersForTimedActions (in workflow-impl) @NotNull public List<ItemDelta<?, ?>> createTriggersForTimedActions(String campaignOid, int escalationLevel, Date workItemCreateTime, Date workItemDeadline, List<WorkItemTimedActionsType> timedActionsList) { LOGGER.trace("Creating triggers for timed actions for certification campaign {}, escalation level {}, create time {}, deadline {}, {} timed action(s)", campaignOid, escalationLevel, workItemCreateTime, workItemDeadline, timedActionsList.size()); try { List<TriggerType> triggers = WfContextUtil.createTriggers(escalationLevel, workItemCreateTime, workItemDeadline, timedActionsList, prismContext, LOGGER, null, AccCertTimedActionTriggerHandler.HANDLER_URI); LOGGER.trace("Created {} triggers for campaign {}:\n{}", triggers.size(), campaignOid, PrismUtil.serializeQuietlyLazily(prismContext, triggers)); if (triggers.isEmpty()) { return Collections.emptyList(); } else { return DeltaBuilder.deltaFor(AccessCertificationCampaignType.class, prismContext) .item(TaskType.F_TRIGGER).add(PrismContainerValue.toPcvList(triggers)) .asItemDeltas(); } } catch (SchemaException | RuntimeException e) { throw new SystemException("Couldn't create trigger(s) for campaign " + campaignOid + ": " + e.getMessage(), e); } } //endregion //region ================================ Campaign and stage close ================================ void closeCampaign(AccessCertificationCampaignType campaign, Task task, OperationResult result) throws ObjectAlreadyExistsException, ObjectNotFoundException, SchemaException, SecurityViolationException { LOGGER.info("Closing campaign {}", ObjectTypeUtil.toShortString(campaign)); XMLGregorianCalendar now = XmlTypeConverter.createXMLGregorianCalendar(new Date()); int lastStageNumber = CertCampaignTypeUtil.getNumberOfStages(campaign); // TODO issue a warning if we are not in a correct state List<ItemDelta<?, ?>> deltas = new ArrayList<>(); deltas.add(createStageNumberDelta(lastStageNumber + 1)); deltas.add(createStateDelta(CLOSED)); deltas.add(createTriggerDeleteDelta()); deltas.add(createEndTimeDelta(now)); deltas.addAll(createWorkItemsCloseDeltas(campaign, now, result)); modifyObjectViaModel(AccessCertificationCampaignType.class, campaign.getOid(), deltas, task, result); AccessCertificationCampaignType updatedCampaign = refreshCampaign(campaign, result); LOGGER.info("Updated campaign state: {}", updatedCampaign.getState()); eventHelper.onCampaignEnd(updatedCampaign, task, result); if (campaign.getDefinitionRef() != null) { List<ItemDelta<?,?>> definitionDeltas = DeltaBuilder.deltaFor(AccessCertificationDefinitionType.class, prismContext) .item(F_LAST_CAMPAIGN_CLOSED_TIMESTAMP).replace(now) .asItemDeltas(); modifyObjectViaModel(AccessCertificationDefinitionType.class, campaign.getDefinitionRef().getOid(), definitionDeltas, task, result); } } private Collection<ItemDelta<?, ?>> createWorkItemsCloseDeltas(AccessCertificationCampaignType campaign, XMLGregorianCalendar now, OperationResult result) throws SchemaException, ObjectNotFoundException { ObjectQuery query = CertCampaignTypeUtil.createWorkItemsForCampaignQuery(campaign.getOid(), prismContext); List<AccessCertificationWorkItemType> openWorkItems = queryHelper.searchOpenWorkItems(query, null, false, null, result); LOGGER.debug("There are {} open work items for {}", openWorkItems.size(), ObjectTypeUtil.toShortString(campaign)); Collection<ItemDelta<?, ?>> deltas = new ArrayList<>(); for (AccessCertificationWorkItemType workItem : openWorkItems) { AccessCertificationCaseType aCase = CertCampaignTypeUtil.getCaseChecked(workItem); deltas.add( DeltaBuilder.deltaFor(AccessCertificationCampaignType.class, prismContext) .item(F_CASE, aCase.getId(), F_WORK_ITEM, workItem.getId(), F_CLOSE_TIMESTAMP) .replace(now) .asItemDelta()); } return deltas; } List<ItemDelta<?,?>> getDeltasForStageClose(AccessCertificationCampaignType campaign, OperationResult result) throws ObjectNotFoundException, SchemaException, ObjectAlreadyExistsException { XMLGregorianCalendar now = XmlTypeConverter.createXMLGregorianCalendar(new Date()); List<ItemDelta<?,?>> rv = caseHelper.createOutcomeDeltas(campaign, result); rv.add(createStateDelta(REVIEW_STAGE_DONE)); rv.add(createStageEndTimeDelta(campaign, now)); rv.add(createTriggerDeleteDelta()); rv.addAll(createWorkItemsCloseDeltas(campaign, now, result)); return rv; } private ItemDelta createStageEndTimeDelta(AccessCertificationCampaignType campaign, XMLGregorianCalendar now) throws SchemaException { AccessCertificationStageType stage = CertCampaignTypeUtil.findStage(campaign, campaign.getStageNumber()); Long stageId = stage.asPrismContainerValue().getId(); assert stageId != null; return DeltaBuilder.deltaFor(AccessCertificationCampaignType.class, prismContext) .item(F_STAGE, stageId, F_END_TIMESTAMP).replace(now) .asItemDelta(); } void afterStageClose(String campaignOid, Task task, OperationResult result) throws SchemaException, ObjectNotFoundException { final AccessCertificationCampaignType campaign = generalHelper.getCampaign(campaignOid, null, task, result); eventHelper.onCampaignStageEnd(campaign, task, result); } //endregion //region ================================ Auxiliary methods for delta processing ================================ List<ItemDelta<?,?>> createDeltasForStageNumberAndState(int number, AccessCertificationCampaignStateType state) { final List<ItemDelta<?,?>> rv = new ArrayList<>(); rv.add(createStageNumberDelta(number)); rv.add(createStateDelta(state)); return rv; } private PropertyDelta<Integer> createStageNumberDelta(int number) { return PropertyDelta.createReplaceDelta(generalHelper.getCampaignObjectDefinition(), AccessCertificationCampaignType.F_STAGE_NUMBER, number); } private PropertyDelta<AccessCertificationCampaignStateType> createStateDelta(AccessCertificationCampaignStateType state) { return PropertyDelta.createReplaceDelta(generalHelper.getCampaignObjectDefinition(), AccessCertificationCampaignType.F_STATE, state); } private PropertyDelta<XMLGregorianCalendar> createStartTimeDelta(XMLGregorianCalendar date) { return PropertyDelta.createReplaceDelta(generalHelper.getCampaignObjectDefinition(), AccessCertificationCampaignType.F_START_TIMESTAMP, date); } private PropertyDelta<XMLGregorianCalendar> createEndTimeDelta(XMLGregorianCalendar date) { return PropertyDelta.createReplaceDelta(generalHelper.getCampaignObjectDefinition(), AccessCertificationCampaignType.F_END_TIMESTAMP, date); } private ContainerDelta createTriggerDeleteDelta() { return ContainerDelta.createModificationReplace(ObjectType.F_TRIGGER, generalHelper.getCampaignObjectDefinition()); } private ItemDelta createStageAddDelta(AccessCertificationStageType stage) { ContainerDelta<AccessCertificationStageType> stageDelta = ContainerDelta.createDelta(F_STAGE, AccessCertificationCampaignType.class, prismContext); stageDelta.addValueToAdd(stage.asPrismContainerValue()); return stageDelta; } //endregion //region ================================ Model and repository operations ================================ void addObject(ObjectType objectType, Task task, OperationResult result) throws ObjectAlreadyExistsException, SchemaException, ObjectNotFoundException { ObjectDelta<? extends ObjectType> objectDelta = ObjectDelta.createAddDelta(objectType.asPrismObject()); Collection<ObjectDeltaOperation<? extends ObjectType>> ops; try { ops = modelService.executeChanges( Collections.singleton(objectDelta), ModelExecuteOptions.createRaw().setPreAuthorized(), task, result); } catch (ExpressionEvaluationException|CommunicationException|ConfigurationException|PolicyViolationException|SecurityViolationException e) { throw new SystemException("Unexpected exception when adding object: " + e.getMessage(), e); } ObjectDeltaOperation odo = ops.iterator().next(); objectType.setOid(odo.getObjectDelta().getOid()); /* ALTERNATIVELY, we can go directly into the repository. (No audit there.) String oid = repositoryService.addObject(objectType.asPrismObject(), null, result); objectType.setOid(oid); */ } <T extends ObjectType> void modifyObjectViaModel(Class<T> objectClass, String oid, Collection<ItemDelta<?,?>> itemDeltas, Task task, OperationResult result) throws ObjectAlreadyExistsException, SchemaException, ObjectNotFoundException { ObjectDelta<T> objectDelta = ObjectDelta.createModifyDelta(oid, itemDeltas, objectClass, prismContext); try { ModelExecuteOptions options = ModelExecuteOptions.createRaw().setPreAuthorized(); modelService.executeChanges(Collections.singletonList(objectDelta), options, task, result); } catch (SecurityViolationException|ExpressionEvaluationException|CommunicationException|ConfigurationException|PolicyViolationException e) { throw new SystemException("Unexpected exception when modifying " + objectClass.getSimpleName() + " " + oid + ": " + e.getMessage(), e); } } // <T extends ObjectType> void modifyObject(Class<T> objectClass, String oid, Collection<ItemDelta> itemDeltas, OperationResult result) throws SchemaException, ObjectAlreadyExistsException, ObjectNotFoundException { // repositoryService.modifyObject(objectClass, oid, itemDeltas, result); // } // TODO implement more efficiently public AccessCertificationCampaignType refreshCampaign(AccessCertificationCampaignType campaign, OperationResult result) throws ObjectNotFoundException, SchemaException, SecurityViolationException { return repositoryService.getObject(AccessCertificationCampaignType.class, campaign.getOid(), null, result).asObjectable(); } //endregion }