/*
* 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.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.PrimitiveIterator.OfInt;
import java.util.Set;
import java.util.UUID;
import javax.inject.Singleton;
//~--- non-JDK imports --------------------------------------------------------
import org.jvnet.hk2.annotations.Service;
import sh.isaac.api.Get;
import sh.isaac.api.LookupService;
import sh.isaac.api.State;
import sh.isaac.api.chronicle.ObjectChronologyType;
import sh.isaac.api.commit.CommitRecord;
import sh.isaac.api.commit.Stamp;
import sh.isaac.api.component.concept.ConceptChronology;
import sh.isaac.api.component.concept.ConceptVersion;
import sh.isaac.api.component.sememe.SememeChronology;
import sh.isaac.api.component.sememe.version.ComponentNidSememe;
import sh.isaac.api.component.sememe.version.DescriptionSememe;
import sh.isaac.api.component.sememe.version.DynamicSememe;
import sh.isaac.api.component.sememe.version.LogicGraphSememe;
import sh.isaac.api.component.sememe.version.LongSememe;
import sh.isaac.api.component.sememe.version.MutableComponentNidSememe;
import sh.isaac.api.component.sememe.version.MutableDescriptionSememe;
import sh.isaac.api.component.sememe.version.MutableDynamicSememe;
import sh.isaac.api.component.sememe.version.MutableLogicGraphSememe;
import sh.isaac.api.component.sememe.version.MutableLongSememe;
import sh.isaac.api.component.sememe.version.MutableStringSememe;
import sh.isaac.api.component.sememe.version.SememeVersion;
import sh.isaac.api.component.sememe.version.StringSememe;
import sh.isaac.api.coordinate.EditCoordinate;
import sh.isaac.api.identity.StampedVersion;
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.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 update existing workflow content after
* initialization aside from launching or ending them.
*
* {@link WorkflowContentStore} {@link WorkflowProvider}
* {@link BPMNInfo}
*
* @author <a href="mailto:jefron@westcoastinformatics.com">Jesse Efron</a>
*/
@Service
@Singleton
public class WorkflowUpdater {
/** The workflow provider. */
private final WorkflowProvider workflowProvider;
//~--- constructors --------------------------------------------------------
/**
* Instantiates a new workflow updater.
*/
// For HK2
private WorkflowUpdater() {
this.workflowProvider = LookupService.get()
.getService(WorkflowProvider.class);
}
//~--- methods -------------------------------------------------------------
/**
* Attempts to add components associated with a commit to a process. Can
* only be done if the process and component are in the process state as
* defined by addComponentToWorkflow. Does so for all concepts and sememes
* in the commit record as well as the commit record's stamp sequence .
*
* Called by the REST implement commit() methods.
*
* @param processId
* The process to which a commit record is being added
* @param commitRecord
* The commit record being associated with the process
*
* @throws Exception
* Thrown if process doesn't exist,
*/
public void addCommitRecordToWorkflow(UUID processId, Optional<CommitRecord> commitRecord)
throws Exception {
if (commitRecord.isPresent()) {
final ProcessDetail detail = this.workflowProvider.getProcessDetailStore()
.get(processId);
final OfInt conceptItr = Get.identifierService()
.getConceptNidsForConceptSequences(commitRecord.get()
.getConceptsInCommit()
.parallelStream())
.iterator();
final OfInt sememeItr = Get.identifierService()
.getSememeNidsForSememeSequences(commitRecord.get()
.getSememesInCommit()
.parallelStream())
.iterator();
while (conceptItr.hasNext()) {
final int conNid = conceptItr.next();
if (isModifiableComponentInProcess(detail, conNid)) {
addComponentToWorkflow(detail, conNid, commitRecord.get());
} else {
// TODO: Prevention strategy for when component not
// deemed "addable" to WF
throw new Exception("Concept may not be added to Workflow: " + conNid);
}
}
while (sememeItr.hasNext()) {
final int semNid = sememeItr.next();
if (isModifiableComponentInProcess(detail, semNid)) {
addComponentToWorkflow(detail, semNid, commitRecord.get());
} else {
// TODO: Prevention strategy for when component not
// deemed "addable" to WF
throw new Exception("Sememe may not be added to Workflow: " + semNid);
}
}
}
}
/**
* Associates a component with a process. If the comoponent has already been associated, nothing to do. Otherwise, add the component and the
* timestamp of the edit to know the last version prior to editing
*
* Note: Made public to enable unit testing
*
* @param process The process to which a component/stamp pair is being added
* @param compNid The component being added
* @param commitRecord the commit record
*/
public void addComponentToWorkflow(ProcessDetail process, int compNid, CommitRecord commitRecord) {
if (!process.getComponentToInitialEditMap()
.keySet()
.contains(compNid)) {
final int stampSeq = commitRecord.getStampsInCommit()
.getIntIterator()
.next();
final State status = Get.stampService()
.getStatusForStamp(stampSeq);
final long time = Get.stampService()
.getTimeForStamp(stampSeq);
final int author = Get.stampService()
.getAuthorSequenceForStamp(stampSeq);
final int module = Get.stampService()
.getModuleSequenceForStamp(stampSeq);
final int path = Get.stampService()
.getPathSequenceForStamp(stampSeq);
final Stamp componentStamp = new Stamp(status, time, author, module, path);
process.getComponentToInitialEditMap()
.put(compNid, componentStamp);
this.workflowProvider.getProcessDetailStore()
.put(process.getId(), process);
}
}
/**
* Advance an existing process with the specified action. In doing so, the
* user must add an advancement comment.
*
* Used by filling in the information prompted for after selecting a
* Transition Workflow action.
*
* @param processId The process being advanced.
* @param userId The user advancing the process.
* @param actionRequested The advancement action the user requested.
* @param comment The comment added by the user in advancing the process.
* @param editCoordinate the edit coordinate
* @return True if the advancement attempt was successful
* @throws Exception Thrown if the requested action was to launch or end a process
* and while updating the process accordingly, an execption
* occurred
*/
public boolean advanceWorkflow(UUID processId,
UUID userId,
String actionRequested,
String comment,
EditCoordinate editCoordinate)
throws Exception {
// Get User Permissible actions
final Set<AvailableAction> userPermissableActions = this.workflowProvider.getWorkflowAccessor()
.getUserPermissibleActionsForProcess(
processId,
userId);
// Advance Workflow
for (final AvailableAction action: userPermissableActions) {
if (action.getAction()
.equals(actionRequested)) {
final ProcessDetail process = this.workflowProvider.getProcessDetailStore()
.get(processId);
// Update Process Details for launch, cancel, or conclude
if (this.workflowProvider.getBPMNInfo()
.getEndWorkflowTypeMap()
.get(EndWorkflowType.CANCELED)
.contains(action)) {
// Request to cancel workflow
this.workflowProvider.getWorkflowProcessInitializerConcluder()
.endWorkflowProcess(processId,
action,
userId,
comment,
EndWorkflowType.CANCELED,
editCoordinate);
} else if (process.getStatus()
.equals(ProcessStatus.DEFINED)) {
for (final AvailableAction startAction: this.workflowProvider.getBPMNInfo()
.getDefinitionStartActionMap()
.get(process.getDefinitionId())) {
if (startAction.getOutcomeState()
.equals(action.getInitialState())) {
// Advancing request is to launch workflow
this.workflowProvider.getWorkflowProcessInitializerConcluder()
.launchProcess(processId);
break;
}
}
} else if (this.workflowProvider.getBPMNInfo()
.getEndWorkflowTypeMap()
.get(EndWorkflowType.CONCLUDED)
.contains(action)) {
// Conclude Request made
this.workflowProvider.getWorkflowProcessInitializerConcluder()
.endWorkflowProcess(processId,
action,
userId,
comment,
EndWorkflowType.CONCLUDED,
null);
} else {
// Generic Advancement. Must still update Detail Store to automate releasing of instance
final ProcessDetail entry = this.workflowProvider.getProcessDetailStore()
.get(processId);
entry.setOwnerId(BPMNInfo.UNOWNED_PROCESS);
this.workflowProvider.getProcessDetailStore()
.put(processId, entry);
}
// Add to process history
final ProcessHistory hx = this.workflowProvider.getWorkflowAccessor()
.getProcessHistory(processId)
.last();
final ProcessHistory entry = new ProcessHistory(processId,
userId,
new Date().getTime(),
action.getInitialState(),
action.getAction(),
action.getOutcomeState(),
comment,
hx.getHistorySequence() + 1);
this.workflowProvider.getProcessHistoryStore()
.add(entry);
return true;
}
}
return false;
}
/**
* Removes a component from a process where the component had been
* previously saved and associated with. In doing so, reverts the component
* to its original state prior to the saves associated with the component.
* The revert is performed by adding new versions to ensure that the
* component attributes are identical prior to any modification associated
* with the process. Note that nothing prevents future edits to be performed
* upon the component associated with the same process.
*
* Used when component is removed from the process's component details panel
*
* @param processId The process from which the component is to be removed
* @param compNid The component whose changes are to be reverted and removed
* from the process
* @param editCoordinate the edit coordinate
* @throws Exception Thrown if the component has been found to not be currently
* associated with the process
*/
public void removeComponentFromWorkflow(UUID processId,
int compNid,
EditCoordinate editCoordinate)
throws Exception {
final ProcessDetail detail = this.workflowProvider.getProcessDetailStore()
.get(processId);
if (isModifiableComponentInProcess(detail, compNid)) {
if (!detail.getComponentToInitialEditMap()
.keySet()
.contains(compNid)) {
throw new Exception("Component " + compNid + " is not already in Workflow");
}
revertChanges(Arrays.asList(compNid), processId, editCoordinate);
detail.getComponentToInitialEditMap()
.remove(compNid);
this.workflowProvider.getProcessDetailStore()
.put(processId, detail);
} else {
throw new Exception("Components may not be removed from Workflow: " + compNid);
}
}
/**
* Revert changes.
*
* @param compNidSet the comp nid set
* @param processId the process id
* @param editCoordinate the edit coordinate
* @throws Exception the exception
*/
protected void revertChanges(Collection<Integer> compNidSet,
UUID processId,
EditCoordinate editCoordinate)
throws Exception {
if (editCoordinate != null) {
for (final Integer compNid: compNidSet) {
final StampedVersion version = this.workflowProvider.getWorkflowAccessor()
.getVersionPriorToWorkflow(processId, compNid);
// add new version identical to version associated with
// actualStampSeq
if (Get.identifierService()
.getChronologyTypeForNid(compNid) == ObjectChronologyType.CONCEPT) {
final ConceptChronology<?> conceptChron = Get.conceptService()
.getConcept(compNid);
if (version != null) {
// conceptChron = ((ConceptVersion) version).getChronology();
conceptChron.createMutableVersion(((ConceptVersion<?>) version).getState(), editCoordinate);
} else {
conceptChron.createMutableVersion(State.INACTIVE, editCoordinate);
}
Get.commitService()
.addUncommitted(conceptChron);
Get.commitService()
.commit("Reverting concept to how it was prior to workflow");
} else if (Get.identifierService()
.getChronologyTypeForNid(compNid) == ObjectChronologyType.SEMEME) {
final SememeChronology<?> semChron = Get.sememeService()
.getSememe(compNid);
if (version != null) {
SememeVersion createdVersion = ((SememeChronology) semChron).createMutableVersion(version.getClass(),
((SememeVersion<?>) version).getState(),
editCoordinate);
createdVersion = populateData(createdVersion, (SememeVersion<?>) version);
} else {
final List<SememeVersion> list = ((SememeChronology) semChron).getVersionList();
final SememeVersion lastVersion = list.toArray(new SememeVersion[list.size()])[list.size() - 1];
SememeVersion createdVersion =
((SememeChronology) semChron).createMutableVersion(lastVersion.getClass(),
State.INACTIVE,
editCoordinate);
createdVersion = populateData(createdVersion, lastVersion);
}
Get.commitService()
.addUncommitted(semChron)
.get();
Get.commitService()
.commit("Reverting sememe to how it was prior to workflow")
.get();
}
}
}
}
/**
* Populate data.
*
* @param newVer the new ver
* @param originalVersion the original version
* @return the sememe version
* @throws Exception the exception
*/
private SememeVersion<?> populateData(SememeVersion<?> newVer, SememeVersion<?> originalVersion)
throws Exception {
switch (newVer.getChronology()
.getSememeType()) {
case MEMBER:
return newVer;
case COMPONENT_NID:
((MutableComponentNidSememe<?>) newVer).setComponentNid(
((ComponentNidSememe<?>) originalVersion).getComponentNid());
return newVer;
case DESCRIPTION:
((MutableDescriptionSememe<?>) newVer).setText(((DescriptionSememe<?>) originalVersion).getText());
((MutableDescriptionSememe<?>) newVer).setDescriptionTypeConceptSequence(
((DescriptionSememe<?>) originalVersion).getDescriptionTypeConceptSequence());
((MutableDescriptionSememe<?>) newVer).setCaseSignificanceConceptSequence(
((DescriptionSememe<?>) originalVersion).getCaseSignificanceConceptSequence());
((MutableDescriptionSememe<?>) newVer).setLanguageConceptSequence(
((DescriptionSememe<?>) originalVersion).getLanguageConceptSequence());
return newVer;
case DYNAMIC:
((MutableDynamicSememe<?>) newVer).setData(((DynamicSememe<?>) originalVersion).getData());
return newVer;
case LONG:
((MutableLongSememe<?>) newVer).setLongValue(((LongSememe<?>) originalVersion).getLongValue());
return newVer;
case STRING:
((MutableStringSememe<?>) newVer).setString(((StringSememe<?>) originalVersion).getString());
return newVer;
case RELATIONSHIP_ADAPTOR:
throw new Exception("Cannot handle Relationship adaptors at this time");
/*
* RelationshipVersionAdaptorImpl origRelVer =
* (RelationshipVersionAdaptorImpl) originalVersion;
* RelationshipAdaptorChronicleKeyImpl key = new
* RelationshipAdaptorChronicleKeyImpl(
* origRelVer.getOriginSequence(),
* origRelVer.getDestinationSequence(),
* origRelVer.getTypeSequence(), origRelVer.getGroup(),
* origRelVer.getPremiseType(), origRelVer.getNodeSequence());
*
* return new RelationshipVersionAdaptorImpl(key, inactiveStampSeq);
*/
case LOGIC_GRAPH:
((MutableLogicGraphSememe<?>) newVer).setGraphData(((LogicGraphSememe<?>) originalVersion).getGraphData());
return newVer;
case UNKNOWN:
throw new UnsupportedOperationException();
}
return null;
}
//~--- get methods ---------------------------------------------------------
/**
* Identifies if process is in an edit state. May only be done if either the
* component is not in any workflow or if it is already in this process's
* workflow AND one of the following: a) Process status is DEFINED or b)
* process status is LAUNCHED while its latestHistory's Outcome is an
* Editing state.
*
* Used by addCommitRecordToWorkflow() and removeComponentFromWorfklow() to
* ensure that the process is in a valid state to be performing such an
* action
*
* @param process
* The process being investigated
* @param compNid
* The component to be added/removed
*
* @return True if the component can be added or removed from the process
*
* @throws Exception
* Thrown if process doesn't exist,
*/
private boolean isModifiableComponentInProcess(ProcessDetail process, int compNid)
throws Exception {
if (process == null) {
throw new Exception("Cannot examine modification capability as the process doesn't exist");
}
final UUID processId = process.getId();
// Check if in Case A. If not, throw exception
if (this.workflowProvider.getWorkflowAccessor().isComponentInActiveWorkflow(process.getDefinitionId(),
compNid) &&
!process.getComponentToInitialEditMap().keySet().contains(compNid)) {
// Can't do so because component is already in another active
// workflow
return false;
}
boolean canAddComponent = false;
// Test Case B
if (process.getStatus() == ProcessStatus.DEFINED) {
canAddComponent = true;
} else {
// Test Case C
if (process.getStatus() == ProcessStatus.LAUNCHED) {
final ProcessHistory latestHx = this.workflowProvider.getWorkflowAccessor()
.getProcessHistory(processId)
.last();
if (this.workflowProvider.getBPMNInfo()
.isEditState(process.getDefinitionId(), latestHx.getOutcomeState())) {
canAddComponent = true;
}
}
}
if (!canAddComponent) {
if (!process.isActive()) {
// Cannot do so because process is not active
return false;
} else {
// Cannot do so because process is in LAUNCHED state yet the
// workflow is not in an EDIT state
return false;
}
}
return true;
}
//~--- set methods ---------------------------------------------------------
/**
* Sets the process owner. When the owner equals BPMNInfo.UNOWNED_PROCESS,
* means process not owned by anyone
*
* @param processId
* the process id to be updated
* @param newOwner
* the new owner. If lock is being acquired, send userId. If
* being released, to BPMNInfo.UNOWNED_PROCESS
*/
public void setProcessOwner(UUID processId, UUID newOwner) {
final ProcessDetail process = this.workflowProvider.getProcessDetailStore()
.get(processId);
process.setOwnerId(newOwner);
this.workflowProvider.getProcessDetailStore()
.put(process.getId(), process);
}
}