/*
* 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.
*
* Contributions from 2013-2017 where performed either by US government
* employees, or under US Veterans Health Administration contracts.
*
* US Veterans Health Administration contributions by government employees
* are work of the U.S. Government and are not subject to copyright
* protection in the United States. Portions contributed by government
* employees are USGovWork (17USC ยง105). Not subject to copyright.
*
* Contribution by contractors to the US Veterans Health Administration
* during this period are contractually contributed under the
* Apache License, Version 2.0.
*
* See: https://www.usa.gov/government-works
*
* Contributions prior to 2013:
*
* Copyright (C) International Health Terminology Standards Development Organisation.
* Licensed under the Apache License, Version 2.0.
*
*/
package sh.isaac.provider.workflow.crud;
//~--- JDK imports ------------------------------------------------------------
import java.util.Date;
import java.util.UUID;
import javax.inject.Singleton;
//~--- non-JDK imports --------------------------------------------------------
import org.jvnet.hk2.annotations.Service;
import sh.isaac.api.LookupService;
import sh.isaac.api.coordinate.EditCoordinate;
import sh.isaac.provider.workflow.BPMNInfo;
import sh.isaac.provider.workflow.WorkflowProvider;
import sh.isaac.provider.workflow.model.WorkflowContentStore;
import sh.isaac.provider.workflow.model.contents.AvailableAction;
import sh.isaac.provider.workflow.model.contents.DefinitionDetail;
import sh.isaac.provider.workflow.model.contents.ProcessDetail;
import sh.isaac.provider.workflow.model.contents.ProcessDetail.EndWorkflowType;
import sh.isaac.provider.workflow.model.contents.ProcessDetail.ProcessStatus;
import sh.isaac.provider.workflow.model.contents.ProcessHistory;
//~--- classes ----------------------------------------------------------------
/**
* Contains methods necessary to start, launch, cancel, or conclude a workflow
* process
*
*
* {@link WorkflowContentStore} {@link WorkflowProvider}
* {@link BPMNInfo}.
*
* @author <a href="mailto:jefron@westcoastinformatics.com">Jesse Efron</a>
*/
@Service
@Singleton
public class WorkflowProcessInitializerConcluder {
/** The workflow provider. */
private final WorkflowProvider workflowProvider;
//~--- constructors --------------------------------------------------------
/**
* Instantiates a new workflow process initializer concluder.
*/
// for HK2
private WorkflowProcessInitializerConcluder() {
this.workflowProvider = LookupService.get()
.getService(WorkflowProvider.class);
}
//~--- methods -------------------------------------------------------------
/**
* Creates a new workflow process instance. In turn, a new entry is added to the ProcessDetails content store. The process status defaults as
* DEFINED.
*
* Used by users when creating a new process
*
* @param definitionId The definition for which the process should be based on
* @param userId The user whom is creating the new process
* @param name The name of the new process
* @param description The description of the new process
* @return The process id which is in turn the key to the Process Detail's entry
* @throws Exception the exception
*/
public UUID createWorkflowProcess(UUID definitionId, UUID userId, String name, String description)
throws Exception {
if ((name == null) || name.isEmpty() || (description == null) || description.isEmpty()) {
throw new Exception("Name and Description must be filled out when creating a process");
}
// TODO: Do we actually want this prevention measure?
/*
* for (ProcessDetail detail : processDetailStore.getAllEntries()) { if
* (detail.getName().equalsIgnoreCase(name)) { throw new
* Exception("Process names must be unique"); } }
*/
// Create Process Details with "DEFINED"
final ProcessDetail details = new ProcessDetail(definitionId,
userId,
new Date().getTime(),
ProcessStatus.DEFINED,
name,
description);
final UUID processId = this.workflowProvider.getProcessDetailStore()
.add(details);
// Add Process History with START_STATE-AUTOMATED-EDIT_STATE
// At some point, need to handle the case where multiple startActions
// may be defined for single DefinitionId. For now, verify only one and
// use it
if (this.workflowProvider.getBPMNInfo()
.getDefinitionStartActionMap()
.get(definitionId)
.size() != 1) {
throw new Exception(
"Currently only able to handle single startAction within a definition. This definition found: " +
this.workflowProvider.getBPMNInfo().getDefinitionStartActionMap().get(definitionId).size());
}
final AvailableAction startAdvancement = this.workflowProvider.getBPMNInfo()
.getDefinitionStartActionMap()
.get(definitionId)
.iterator()
.next();
final ProcessHistory advanceEntry = new ProcessHistory(processId,
userId,
new Date().getTime(),
startAdvancement.getInitialState(),
startAdvancement.getAction(),
startAdvancement.getOutcomeState(),
"",
1);
this.workflowProvider.getProcessHistoryStore()
.add(advanceEntry);
return processId;
}
/**
* Ends a workflow instance either via concluding it or canceling it. In doing so, the ProcessDetails is updated accordingly, another Process
* History entry is added showing the advancement, and in the case of "CANCEL" request, any editing changes previously associated with the
* instance are reverted.
*
* @param processId The process being ended
* @param actionToProcess The AvailableAction the user requested
* @param userId The user ending the workflow
* @param comment The user added comment associated with the advancement
* @param endType The type of END-ADVANCEMENT associated with the selected
* action (Canceled or Concluded)
* @param editCoordinate the edit coordinate
* @throws Exception Thrown if the process doesn't exist or an attempt is made to a) cancel or conclude a process which isn't active, b) conclude a process where
* the process is not LAUNCHED, or c) conclude a process where the outcome state isn't a concluded state according to the definition
*/
public void endWorkflowProcess(UUID processId,
AvailableAction actionToProcess,
UUID userId,
String comment,
EndWorkflowType endType,
EditCoordinate editCoordinate)
throws Exception {
final ProcessDetail entry = this.workflowProvider.getProcessDetailStore()
.get(processId);
final ProcessHistory hx = this.workflowProvider.getWorkflowAccessor()
.getProcessHistory(processId)
.last();
if (entry == null) {
throw new Exception("Cannot cancel nor conclude a workflow that hasn't been defined yet");
} else if (!entry.isActive()) {
throw new Exception("Cannot end a workflow that is not active. Current status: " + entry.getStatus());
} else if (endType == EndWorkflowType.CONCLUDED) {
if (entry.getStatus() != ProcessStatus.LAUNCHED) {
throw new Exception("Cannot conclude workflow that is in the following state: " + entry.getStatus());
} else {
if (!this.workflowProvider.getBPMNInfo()
.isConcludedState(hx.getOutcomeState())) {
final DefinitionDetail defEntry = this.workflowProvider.getDefinitionDetailStore()
.get(entry.getDefinitionId());
throw new Exception("Cannot perform Conclude action on the definition: " + defEntry.getName() +
" version: " + defEntry.getVersion() + " when the workflow state is: " +
hx.getOutcomeState());
}
}
}
// Request is valid, update process details
entry.setOwnerId(BPMNInfo.UNOWNED_PROCESS);
entry.setTimeCanceledOrConcluded(new Date().getTime());
if (endType.equals(EndWorkflowType.CANCELED)) {
entry.setStatus(ProcessStatus.CANCELED);
} else if (endType.equals(EndWorkflowType.CONCLUDED)) {
entry.setStatus(ProcessStatus.CONCLUDED);
}
this.workflowProvider.getProcessDetailStore()
.put(processId, entry);
// Add to process's history
final ProcessHistory advanceEntry = new ProcessHistory(processId,
userId,
new Date().getTime(),
actionToProcess.getInitialState(),
actionToProcess.getAction(),
actionToProcess.getOutcomeState(),
comment,
hx.getHistorySequence() + 1);
this.workflowProvider.getProcessHistoryStore()
.add(advanceEntry);
// if a cancel has been requested, revert all changes associated with the workflow
if (endType.equals(EndWorkflowType.CANCELED)) {
this.workflowProvider.getWorkflowUpdater()
.revertChanges(entry.getComponentToInitialEditMap()
.keySet(), processId, editCoordinate);
}
}
/**
* Launch a process as long as a) the process is in a defined state and b) there are components associated with the process. By launching it, the
* ProcessDetails has its status updated to LAUNCHED and the time launched gets an entry of <NOW>.
*
* Used when a process is advanced and it is in the edit state and is has a
* DEFINED status.
*
* @param processId
* the process being launched
*
* @throws Exception
* Thrown if a) process doesn't exist, b) process exists but is not in the DEFINED status, or c) no components are associated with the process
*/
public void launchProcess(UUID processId)
throws Exception {
final ProcessDetail entry = this.workflowProvider.getProcessDetailStore()
.get(processId);
if (entry == null) {
throw new Exception("Cannot launch workflow that hasn't been defined first");
} else if (entry.getStatus() != ProcessStatus.DEFINED) {
throw new Exception("Only processes that have a DEFINED status may be launched");
} else if (entry.getComponentToInitialEditMap()
.keySet()
.isEmpty()) {
throw new Exception("Workflow can only be launched when the workflow contains components to work on");
}
// Update Process Details with "LAUNCHED"
entry.setOwnerId(BPMNInfo.UNOWNED_PROCESS);
entry.setStatus(ProcessStatus.LAUNCHED);
entry.setTimeLaunched(new Date().getTime());
this.workflowProvider.getProcessDetailStore()
.put(processId, entry);
}
}