/*
* 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.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.PrimitiveIterator.OfInt;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
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.UserRole;
import sh.isaac.api.chronicle.LatestVersion;
import sh.isaac.api.chronicle.ObjectChronology;
import sh.isaac.api.chronicle.ObjectChronologyType;
import sh.isaac.api.commit.Stamp;
import sh.isaac.api.component.sememe.SememeChronology;
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.SememeVersion;
import sh.isaac.api.component.sememe.version.dynamicSememe.DynamicSememeColumnInfo;
import sh.isaac.api.component.sememe.version.dynamicSememe.DynamicSememeUsageDescription;
import sh.isaac.api.constants.DynamicSememeConstants;
import sh.isaac.api.coordinate.LanguageCoordinate;
import sh.isaac.api.coordinate.StampCoordinate;
import sh.isaac.api.identity.StampedVersion;
import sh.isaac.model.sememe.DynamicSememeUsageDescriptionImpl;
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.ProcessStatus;
import sh.isaac.provider.workflow.model.contents.ProcessHistory;
import sh.isaac.provider.workflow.model.contents.ProcessHistory.ProcessHistoryComparator;
import sh.isaac.utility.Frills;
//~--- classes ----------------------------------------------------------------
/**
* Contains methods necessary to perform workflow-based accessing
*
* {@link WorkflowContentStore} {@link WorkflowProvider}
* {@link BPMNInfo}.
*
* @author <a href="mailto:jefron@westcoastinformatics.com">Jesse Efron</a>
*/
@Service
@Singleton
public class WorkflowAccessor {
/** The workflow provider. */
private final WorkflowProvider workflowProvider;
//~--- constructors --------------------------------------------------------
/**
* Instantiates a new workflow accessor.
*/
// for HK2
private WorkflowAccessor() {
this.workflowProvider = LookupService.get()
.getService(WorkflowProvider.class);
}
//~--- methods -------------------------------------------------------------
/**
* Format string association information.
*
* @param value the value
* @param target the target
* @return the string
*/
private String formatStringAssociationInformation(String value, String target) {
// Association: <Source Component> : <Target Component>
return String.format("Association: %s : %s", value, target);
}
/**
* Format string concept information.
*
* @param nid the nid
* @param stampCoord the stamp coord
* @param langCoord the lang coord
* @return the string
*/
private String formatStringConceptInformation(int nid, StampCoordinate stampCoord, LanguageCoordinate langCoord) {
// Concept: <Concept FSN>
return String.format("Concept: %s",
Frills.getConceptSnapshot(nid, stampCoord, langCoord)
.get()
.getFullySpecifiedDescription()
.get()
.value()
.getText());
}
/**
* Format string description information.
*
* @param descSem the desc sem
* @return the string
*/
private String formatStringDescriptionInformation(LatestVersion<DescriptionSememe> descSem) {
// Description: <Desctipion Text>
return String.format("Description: %s", descSem.value()
.getText());
}
/**
* Format string map information.
*
* @param value the value
* @param target the target
* @return the string
*/
private String formatStringMapInformation(String value, String target) {
// Map: <MapSet FSN>-<Source Code> : <Target Code>
return String.format("Map: %s : %s", value, target);
}
/**
* Format string value information.
*
* @param value the value
* @return the string
*/
private String formatStringValueInformation(String value) {
// Value: <Value Text>
return String.format("Value: %s", value);
}
//~--- get methods ---------------------------------------------------------
/**
* Map the process history to each process for which the user's roles
* enable them to advance workflow based on the process's current state.
* Only active processes can be advanced thus only those processes with such
* a status are returned.
*
* Used to determine which processes to list when the user selects the
* "Author Workflows" link
*
* @param definitionId
* The definition being examined
* @param userId
* The user for whom relevant processes are being determined
*
* @return The map of advanceable processes to their Process History
*/
public Map<ProcessDetail, SortedSet<ProcessHistory>> getAdvanceableProcessInformation(UUID definitionId,
UUID userId) {
final Map<ProcessDetail, SortedSet<ProcessHistory>> processInformation = new HashMap<>();
// Get User Roles
final Map<String, Set<AvailableAction>> actionsByInitialState =
getUserAvailableActionsByInitialState(definitionId,
userId);
// For each ActiveProcesses, see if its current state is "applicable
// current state" and if
for (final ProcessDetail process: this.workflowProvider.getProcessDetailStore()
.values()) {
if (process.isActive() && process.getDefinitionId().equals(definitionId)) {
final SortedSet<ProcessHistory> hx = getProcessHistory(process.getId());
if (actionsByInitialState.containsKey(hx.last()
.getOutcomeState())) {
processInformation.put(process, hx);
}
}
}
return processInformation;
}
/**
* Examines the definition to see if the component is in an active workflow.
* An active workflow is a workflow in either DEFINED or LAUNCHED process
* status.
*
* Used to ensure that a concept or sememe doesn't belong to two active
* processes simultaneously as that is not allowed at this point. If a
* person attempts to do so, they should get a warning that the commit will
* not be added to a workflow.
*
* @param definitionId
* The key the the Definition Detail entry
* @param compNid
* The component to be investigated
*
* @return True if the component is in an active workflow.
*/
public boolean isComponentInActiveWorkflow(UUID definitionId, int compNid) {
for (final ProcessDetail proc: this.workflowProvider.getProcessDetailStore()
.values()) {
if (proc.getDefinitionId().equals(definitionId) &&
proc.isActive() &&
proc.getComponentToInitialEditMap().keySet().contains(compNid)) {
return true;
}
}
return false;
}
/**
* Checks if component in process.
*
* @param processId the process id
* @param componentNid the component nid
* @return true, if component in process
*/
public boolean isComponentInProcess(UUID processId, int componentNid) {
final ProcessDetail process = getProcessDetails(processId);
return (process != null) ? process.getComponentToInitialEditMap()
.containsKey(componentNid)
: false;
}
/**
* Return a String formatted for component modifications. Includes Concept, Description, Map, Association, or Value.
*
* @param nid int - Component id
* @param stampCoord StampCoordinate
* @param langCoord LanguageCoordinate
* @return the component modification
* @exception Exception the exception
*/
private String getComponentModification(int nid,
StampCoordinate stampCoord,
LanguageCoordinate langCoord)
throws Exception {
final ObjectChronologyType oct = Get.identifierService()
.getChronologyTypeForNid(nid);
if (oct == ObjectChronologyType.CONCEPT) {
return formatStringConceptInformation(nid, stampCoord, langCoord);
} else if (oct == ObjectChronologyType.SEMEME) {
final SememeChronology<? extends SememeVersion<?>> sememe = Get.sememeService()
.getSememe(nid);
switch (sememe.getSememeType()) {
case DESCRIPTION:
final LatestVersion<DescriptionSememe> descSem =
(LatestVersion<DescriptionSememe>) ((SememeChronology) sememe).getLatestVersion(LogicGraphSememe.class,
stampCoord)
.get();
return formatStringDescriptionInformation(descSem);
case DYNAMIC:
final LatestVersion<DynamicSememe> dynSem =
(LatestVersion<DynamicSememe>) ((SememeChronology) sememe).getLatestVersion(LogicGraphSememe.class,
stampCoord)
.get();
final int assemblageSeq = dynSem.value()
.getAssemblageSequence();
Get.conceptService()
.getConcept(assemblageSeq);
String target = null;
String value = null;
final DynamicSememeUsageDescription sememeDefinition = DynamicSememeUsageDescriptionImpl.read(nid);
for (final DynamicSememeColumnInfo info: sememeDefinition.getColumnInfo()) {
if (info.getColumnDescriptionConcept()
.equals(DynamicSememeConstants.get().DYNAMIC_SEMEME_COLUMN_VALUE
.getUUID())) {
value = info.getDefaultColumnValue()
.dataToString();
} else if (info.getColumnDescriptionConcept()
.equals(DynamicSememeConstants.get().DYNAMIC_SEMEME_COLUMN_ASSOCIATION_TARGET_COMPONENT
.getUUID())) {
target = info.getDefaultColumnValue()
.dataToString();
}
}
if (Frills.isMapping(sememe)) {
return formatStringMapInformation(value, target);
} else if (Frills.isAssociation(sememe)) {
return formatStringAssociationInformation(value, target);
} else {
return formatStringValueInformation(value);
}
default:
throw new Exception("Unsupported Sememe Type: " + sememe.getSememeType());
}
} else {
throw new Exception("Unsupported Object Chronology Type: " + oct);
}
}
/**
* Return an ArrayList of Strings formatted for component modifications for a given processId.
* Includes Concept, Description, Map, Association, or Value.
*
* @param processId UUID - identifier of the process.
* @return ArrayList<String> - collection of formatted string of component modifications.
* @exception Exception the exception
*/
public ArrayList<String> getComponentModifications(UUID processId)
throws Exception {
// get process detail for process
final ProcessDetail processDetail = getProcessDetails(processId);
final ArrayList<String> componentModificationString = new ArrayList<>();
for (final Map.Entry<Integer, Stamp> entry: processDetail.getComponentToInitialEditMap()
.entrySet()) {
try {
componentModificationString.add(getComponentModification(entry.getKey(),
// Verify these two parameters.
(StampCoordinate) entry.getValue(), (LanguageCoordinate) entry.getValue()));
} catch (final Exception ex) {
throw ex;
}
}
return componentModificationString;
}
/**
* Gets the Definition Detail entry for the specified definition key
*
* Used to access all information associated with a given Workflow
* Definition.
*
* @param definitionId
* The key the the Definition Detail entry
*
* @return The definition details entry requested
*/
public DefinitionDetail getDefinitionDetails(UUID definitionId) {
return this.workflowProvider.getDefinitionDetailStore()
.get(definitionId);
}
/**
* Returns last Process History entry associated with the process id.
* Used to identify the history of a given workflow process (i.e. an
* instance of a definition)
*
* @param processId
* The key the the Process Detail entry
*
* @return the sorted history of the process.
*/
public ProcessHistory getLastProcessHistory(UUID processId) {
for (final ProcessDetail process: this.workflowProvider.getProcessDetailStore()
.values()) {
if (process.getId()
.compareTo(processId) == 0) {
final SortedSet<ProcessHistory> hx = getProcessHistory(process.getId());
return hx.last();
}
}
return null;
}
/**
* Gets the Process Detail entry for the specified process key
*
* Used to access all information associated with a given workflow process
* (i.e. an instance of a definition).
*
* @param processId
* The key the the Process Detail entry
*
* @return The process details entry requested. If none exists, return null
*/
public ProcessDetail getProcessDetails(UUID processId) {
return this.workflowProvider.getProcessDetailStore()
.get(processId);
}
/**
* Returns all Process History entries associated with the process id. This
* contains all the advancements made during the given process. History is
* sorted by advancement time.
*
* Used to identify the history of a given workflow process (i.e. an
* instance of a definition)
*
* @param processId
* The key the the Process Detail entry
*
* @return the sorted history of the process.
*/
public SortedSet<ProcessHistory> getProcessHistory(UUID processId) {
final SortedSet<ProcessHistory> allHistoryForProcess = new TreeSet<>(new ProcessHistoryComparator());
for (final ProcessHistory hx: this.workflowProvider.getProcessHistoryStore()
.values()) {
if (hx.getProcessId()
.equals(processId)) {
allHistoryForProcess.add(hx);
}
}
return allHistoryForProcess;
}
/**
* Request a list of workflow processes. Can request any or all workflow statuses of DEFINED, LAUNCHED, CANCELED or CONCLUDED.
*
* @param definitionId The workflow definition (type) being examined.
* @param userId the user id
* @param status A list of statuses to include.
* @return The list of processes filtered by the status provided.
*/
public ArrayList<ProcessDetail> getProcessInformation(UUID definitionId,
UUID userId,
ArrayList<ProcessStatus> status) {
final ArrayList<ProcessDetail> processes = new ArrayList<>();
// Get User Roles
/*
* Map<String, Set<AvailableAction>> actionsByInitialState = getUserAvailableActionsByInitialState(
* definitionId, userId);
*/
// For each process, see if its current state is "applicable current state"
for (final ProcessDetail process: this.workflowProvider.getProcessDetailStore()
.values()) {
if (process.getDefinitionId().equals(definitionId) && status.contains(process.getStatus())) {
processes.add(process);
}
}
return processes;
}
/**
* Returns the of available actions a user has roles based on the
* definition's possible initial-states
*
* Used to support the getAdvanceableProcessInformation() and
* getUserPermissibleActionsForProcess().
*
* @param definitionId The definition being examined
* @param userId The user is being examined
* @return The set of all Available Actions for each initial state for which
* the user can advance workflow.
*/
private Map<String, Set<AvailableAction>> getUserAvailableActionsByInitialState(UUID definitionId, UUID userId) {
final Map<String, Set<AvailableAction>> applicableActions = new HashMap<>();
// Get User Roles
final Set<UserRole> userRoles = this.workflowProvider.getUserRoleStore()
.getUserRoles(userId);
// Get Map of available actions (by initialState) that can be executed
// based on userRoles
for (final AvailableAction action: this.workflowProvider.getAvailableActionStore()
.values()) {
if (action.getDefinitionId().equals(definitionId) && userRoles.contains(action.getRole())) {
if (!applicableActions.containsKey(action.getInitialState())) {
applicableActions.put(action.getInitialState(), new HashSet<>());
}
applicableActions.get(action.getInitialState())
.add(action);
}
}
return applicableActions;
}
/**
* Identifies the set of Available Actions containing actions which the user
* may take on a given process
*
* Used to determine which actions populate the Transition Workflow picklist.
*
* @param processId The process being examined
* @param userId The user for whom available actions are being identified
* @return A set of AvailableActions defining the actions a user can take on
* the process
*/
public Set<AvailableAction> getUserPermissibleActionsForProcess(UUID processId, UUID userId) {
final ProcessDetail processDetail = getProcessDetails(processId);
if (processDetail != null) {
final ProcessHistory processLatest = getProcessHistory(processId).last();
final Map<String, Set<AvailableAction>> actionsByInitialState =
getUserAvailableActionsByInitialState(processDetail.getDefinitionId(),
userId);
if (actionsByInitialState.containsKey(processLatest.getOutcomeState())) {
return actionsByInitialState.get(processLatest.getOutcomeState());
}
}
return new HashSet<>();
}
/**
* Identify the version of the component prior to workflow process being launched.
*
* @param processId The process being examined
* @param compNid The component to be investigated
* @return The version of the component prior to it entering into workflow. If no version is found, the chronology was created within this workflow process
*/
public StampedVersion getVersionPriorToWorkflow(UUID processId, int compNid) {
final ProcessDetail proc = getProcessDetails(processId);
if (!proc.getComponentToInitialEditMap()
.keySet()
.contains(compNid)) {
return null;
}
final long timeLaunched = proc.getTimeCreated();
ObjectChronology<?> objChron;
if (Get.identifierService()
.getChronologyTypeForNid(compNid) == ObjectChronologyType.CONCEPT) {
objChron = Get.conceptService()
.getConcept(compNid);
} else if (Get.identifierService()
.getChronologyTypeForNid(compNid) == ObjectChronologyType.SEMEME) {
objChron = Get.sememeService()
.getSememe(compNid);
} else {
throw new RuntimeException("Cannot reconcile NID with Identifier Service for nid: " + compNid);
}
final OfInt stampSequencesItr = objChron.getVersionStampSequences()
.iterator();
int stampSeq = -1;
long stampTime = 0;
while (stampSequencesItr.hasNext() && (stampTime < timeLaunched)) {
final int currentStampSeq = stampSequencesItr.next();
final long currentStampTime = Get.stampService()
.getTimeForStamp(currentStampSeq);
if (currentStampTime < timeLaunched) {
stampTime = currentStampTime;
stampSeq = currentStampSeq;
}
}
for (final StampedVersion version: objChron.getVersionList()) {
if (version.getStampSequence() == stampSeq) {
return version;
}
}
return null;
}
/**
* Gets the version prior to workflow.
*
* @param <T> the generic type
* @param versionClazz the version clazz
* @param processId the process id
* @param compNid the comp nid
* @return the version prior to workflow
* @throws Exception the exception
*/
public <T extends StampedVersion> T getVersionPriorToWorkflow(Class<T> versionClazz,
UUID processId,
int compNid)
throws Exception {
final StampedVersion version = getVersionPriorToWorkflow(processId, compNid);
return versionClazz.cast(version);
}
}