/* * Copyright (c) 2010-2013 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.notifications.impl.notifiers; import com.evolveum.midpoint.notifications.api.events.*; import com.evolveum.midpoint.prism.util.PrismUtil; import com.evolveum.midpoint.prism.xml.XmlTypeConverter; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.schema.util.WfContextUtil; import com.evolveum.midpoint.task.api.Task; import com.evolveum.midpoint.util.exception.SchemaException; 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 org.apache.commons.lang3.time.DurationFormatUtils; import org.jetbrains.annotations.Nullable; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.xml.datatype.XMLGregorianCalendar; import java.util.Date; import java.util.List; import java.util.stream.Collectors; import static com.evolveum.midpoint.prism.polystring.PolyString.getOrig; /** * Default implementation of a notifier dealing with workflow events (related to both work items and process instances). * * @author mederly */ @Component public class SimpleWorkflowNotifier extends GeneralNotifier { private static final Trace LOGGER = TraceManager.getTrace(SimpleWorkflowNotifier.class); @PostConstruct public void init() { register(SimpleWorkflowNotifierType.class); } @Override protected boolean quickCheckApplicability(Event event, GeneralNotifierType generalNotifierType, OperationResult result) { if (!(event instanceof WorkflowEvent)) { LOGGER.trace("SimpleWorkflowNotifier is not applicable for this kind of event, continuing in the handler chain; event class = " + event.getClass()); return false; } return true; } @Override protected UserType getDefaultRecipient(Event event, GeneralNotifierType generalNotifierType, OperationResult result) { @Nullable SimpleObjectRef recipientRef; if (event instanceof WorkflowProcessEvent) { recipientRef = event.getRequester(); } else if (event instanceof WorkItemEvent) { recipientRef = ((WorkItemEvent) event).getAssignee(); } else { return null; } ObjectType recipient = functions.getObjectType(recipientRef, false, result); if (recipient instanceof UserType) { return (UserType) recipient; } else { return null; } } @Override protected String getSubject(Event event, GeneralNotifierType generalNotifierType, String transport, Task task, OperationResult result) { if (event instanceof WorkflowProcessEvent) { return event.isAdd() ? "Workflow process instance has been started" : "Workflow process instance has finished"; } else if (event instanceof WorkItemEvent) { return getSubjectFromWorkItemEvent((WorkItemEvent) event, generalNotifierType, transport, task, result); } else { throw new UnsupportedOperationException("Unsupported event type for event=" + event); } } private String getSubjectFromWorkItemEvent(WorkItemEvent event, GeneralNotifierType generalNotifierType, String transport, Task task, OperationResult result) { if (event instanceof WorkItemLifecycleEvent) { if (event.isAdd()) { return "A new work item has been created"; } else if (event.isDelete()) { if (event.getOperationKind() == WorkItemOperationKindType.COMPLETE) { return "Work item has been completed"; } else { return "Work item has been cancelled"; } } else { throw new UnsupportedOperationException("workItemLifecycle event with MODIFY operation is not supported"); } } else if (event instanceof WorkItemAllocationEvent) { if (event.isAdd()) { return "Work item has been allocated to you"; } else if (event.isModify()) { if (event.getOperationKind() == null) { throw new IllegalStateException("Missing operationKind in " + event); } String rv = "Work item will be automatically " + getOperationPastTenseVerb(event.getOperationKind()); if (event.getTimeBefore() != null) { // should always be rv += " in " + DurationFormatUtils.formatDurationWords( event.getTimeBefore().getTimeInMillis(new Date()), true, true); } return rv; } else { return "Work item has been " + getOperationPastTenseVerb(event.getOperationKind()); } } else if (event instanceof WorkItemCustomEvent) { return "A notification about work item"; } else { throw new UnsupportedOperationException("Unsupported event type for event=" + event); } } @Override protected String getBody(Event event, GeneralNotifierType generalNotifierType, String transport, Task task, OperationResult result) throws SchemaException { WorkflowEvent workflowEvent = (WorkflowEvent) event; boolean techInfo = Boolean.TRUE.equals(generalNotifierType.isShowTechnicalInformation()); StringBuilder body = new StringBuilder(); body.append(getSubject(event, generalNotifierType, transport, task, result)); body.append("\n\n"); appendGeneralInformation(body, workflowEvent); // process instance name, work item name, stage, escalation level if (workflowEvent instanceof WorkItemEvent) { WorkItemEvent workItemEvent = (WorkItemEvent) workflowEvent; appendAssigneeInformation(body, workItemEvent, result); appendResultAndOriginInformation(body, workItemEvent, result); appendDeadlineInformation(body, workItemEvent); } else { appendResultInformation(body, workflowEvent, true); } body.append("\nNotification created on: ").append(new Date()).append("\n\n"); if (techInfo) { body.append("----------------------------------------\n"); body.append("Technical information:\n\n"); if (workflowEvent instanceof WorkItemEvent) { WorkItemEvent workItemEvent = (WorkItemEvent) workflowEvent; body.append("WorkItem:\n") .append(PrismUtil.serializeQuietly(prismContext, workItemEvent.getWorkItem())) .append("\n"); } body.append("Workflow context:\n") .append(PrismUtil.serializeQuietly(prismContext, ((WorkflowEvent) event).getWorkflowContext())); } return body.toString(); } private void appendGeneralInformation(StringBuilder sb, WorkflowEvent workflowEvent) { sb.append("Process instance name: ").append(workflowEvent.getProcessInstanceName()).append("\n"); if (workflowEvent instanceof WorkItemEvent) { WorkItemEvent event = (WorkItemEvent) workflowEvent; sb.append("Work item: ").append(event.getWorkItemName()).append("\n"); appendStageInformation(sb, event); appendEscalationInformation(sb, event); } else { appendStageInformation(sb, workflowEvent); } sb.append("\n"); } private boolean appendResultInformation(StringBuilder body, WorkflowEvent workflowEvent, boolean emptyLineAfter) { if (workflowEvent.isDelete() && workflowEvent.isResultKnown()) { body.append("Result: ").append(workflowEvent.isApproved() ? "APPROVED" : "REJECTED").append("\n"); if (emptyLineAfter) { body.append("\n"); } return true; } else { return false; } } private void appendDeadlineInformation(StringBuilder sb, WorkItemEvent event) { WorkItemType workItem = event.getWorkItem(); if (!isDone(event) && workItem.getDeadline() != null) { XMLGregorianCalendar deadline = workItem.getDeadline(); long before = XmlTypeConverter.toMillis(deadline) - System.currentTimeMillis(); long beforeRounded = Math.round((double) before / 60000.0) * 60000L; String beforeWords = DurationFormatUtils.formatDurationWords(Math.abs(beforeRounded), true, true); String beforePhrase; if (beforeRounded > 0) { beforePhrase = " (in " + beforeWords + ")"; } else if (beforeRounded < 0) { beforePhrase = " (" + beforeWords + " ago)"; } else { beforePhrase = ""; } sb.append("Deadline: ").append(formatDateTime(deadline)).append(beforePhrase).append("\n"); sb.append("\n"); } } private void appendResultAndOriginInformation(StringBuilder sb, WorkItemEvent event, OperationResult result) { boolean atLeastOne = appendResultInformation(sb, event, false); WorkItemEventCauseInformationType cause = event.getCause(); if (cause != null && cause.getType() == WorkItemEventCauseTypeType.TIMED_ACTION) { sb.append("Reason: "); if (cause.getDisplayName() != null) { sb.append(cause.getDisplayName()).append(" (timed action)"); } else if (cause.getName() != null) { sb.append(cause.getName()).append(" (timed action)"); } else { sb.append("Timed action"); } sb.append("\n"); atLeastOne = true; } else { SimpleObjectRef initiator = event.getInitiator(); if (initiator != null && !isCancelled(event)) { UserType initiatorFull = (UserType) functions.getObjectType(initiator, true, result); sb.append("Carried out by: ").append(formatUserName(initiatorFull, initiator.getOid())).append("\n"); atLeastOne = true; } } if (atLeastOne) { sb.append("\n"); } } private void appendAssigneeInformation(StringBuilder sb, WorkItemEvent event, OperationResult result) { WorkItemType workItem = event.getWorkItem(); ObjectReferenceType originalAssignee = workItem.getOriginalAssigneeRef(); List<ObjectReferenceType> currentAssignees = workItem.getAssigneeRef(); boolean atLeastOne = false; if (currentAssignees.size() != 1 || !java.util.Objects.equals(originalAssignee.getOid(), currentAssignees.get(0).getOid())) { UserType originalAssigneeObject = (UserType) functions.getObjectType(originalAssignee, true, result); sb.append("Originally allocated to: ").append(formatUserName(originalAssigneeObject, originalAssignee.getOid())).append("\n"); atLeastOne = true; } if (!workItem.getAssigneeRef().isEmpty()) { sb.append("Allocated to"); if (event.getOperationKind() == WorkItemOperationKindType.DELEGATE) { sb.append(event.isAdd() ? " (after delegation)" : " (before delegation)"); } else if (event.getOperationKind() == WorkItemOperationKindType.ESCALATE) { sb.append(event.isAdd() ? " (after escalation)" : " (before escalation)"); } sb.append(": "); sb.append(workItem.getAssigneeRef().stream() .map(ref -> formatUserName(ref, result)) .collect(Collectors.joining(", "))); sb.append("\n"); atLeastOne = true; } if (atLeastOne) { sb.append("\n"); } } // a bit of heuristics... private boolean isDone(WorkItemEvent event) { if (event instanceof WorkItemLifecycleEvent) { return event.isDelete(); } else if (event instanceof WorkItemAllocationEvent) { return event.isDelete() && (event.getOperationKind() == null || event.getOperationKind() == WorkItemOperationKindType.CANCEL || event.getOperationKind() == WorkItemOperationKindType.COMPLETE); } else { return false; } } private boolean isCancelled(WorkItemEvent event) { return (event instanceof WorkItemLifecycleEvent || event instanceof WorkItemAllocationEvent) && event.isDelete() && (event.getOperationKind() == null || event.getOperationKind() == WorkItemOperationKindType.CANCEL); } private String formatUserName(ObjectReferenceType ref, OperationResult result) { UserType user = (UserType) functions.getObjectType(ref, true, result); return formatUserName(user, ref.getOid()); } // TODO implement seriously private String formatDateTime(XMLGregorianCalendar timestamp) { //DateFormatUtils.format(timestamp.toGregorianCalendar(), DateFormatUtils.SMTP_DATETIME_FORMAT.getPattern()); return String.valueOf(XmlTypeConverter.toDate(timestamp)); } private String formatUserName(UserType user, String oid) { if (user == null || (user.getName() == null && user.getFullName() == null)) { return oid; } if (user.getFullName() != null) { return getOrig(user.getFullName()) + " (" + getOrig(user.getName()) + ")"; } else { return getOrig(user.getName()); } } private void appendEscalationInformation(StringBuilder sb, WorkItemEvent workItemEvent) { String info = WfContextUtil.getEscalationLevelInfo(workItemEvent.getWorkItem()); if (info != null) { sb.append("Escalation level: ").append(info).append("\n"); } } private void appendStageInformation(StringBuilder sb, WorkflowEvent workflowEvent) { String info = WfContextUtil.getStageInfo(workflowEvent.getWorkflowContext()); if (info != null) { sb.append("Stage: ").append(info).append("\n"); } } @Override protected Trace getLogger() { return LOGGER; } private String getOperationPastTenseVerb(WorkItemOperationKindType operationKind) { if (operationKind == null) { return "cancelled"; // OK? } switch (operationKind) { case CLAIM: return "claimed"; case RELEASE: return "released"; case COMPLETE: return "completed"; case DELEGATE: return "delegated"; case ESCALATE: return "escalated"; case CANCEL: return "cancelled"; default: throw new IllegalArgumentException("operation kind: " + operationKind); } } }