WorkflowServiceImpl.java
- /*
- * Copyright 2024 Global Crop Diversity Trust
- *
- * 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 org.gringlobal.service.impl;
- import com.google.common.cache.Cache;
- import com.google.common.cache.CacheBuilder;
- import com.querydsl.jpa.impl.JPAQueryFactory;
- import lombok.extern.slf4j.Slf4j;
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.Objects;
- import java.util.Set;
- import java.util.concurrent.TimeUnit;
- import java.util.stream.Collectors;
- import org.apache.commons.collections4.CollectionUtils;
- import org.apache.commons.collections4.MapUtils;
- import org.apache.commons.lang3.StringUtils;
- import org.genesys.blocks.util.CurrentApplicationContext;
- import org.gringlobal.api.exception.InvalidApiUsageException;
- import org.gringlobal.component.elastic.AppContextHelper;
- import org.gringlobal.model.AbstractAction;
- import org.gringlobal.model.AccessionAction;
- import org.gringlobal.model.InventoryAction;
- import org.gringlobal.model.InventoryViabilityAction;
- import org.gringlobal.model.OrderRequestAction;
- import org.gringlobal.model.OrderRequestItemAction;
- import org.gringlobal.model.workflow.QWorkflowTransition;
- import org.gringlobal.model.workflow.Workflow;
- import org.gringlobal.model.workflow.WorkflowActionStep;
- import org.gringlobal.model.workflow.WorkflowEndStep;
- import org.gringlobal.model.workflow.WorkflowHelperMethods;
- import org.gringlobal.model.workflow.WorkflowStartStep;
- import org.gringlobal.model.workflow.WorkflowStep;
- import org.gringlobal.model.workflow.WorkflowTransition;
- import org.gringlobal.persistence.WorkflowRepository;
- import org.gringlobal.persistence.WorkflowStepRepository;
- import org.gringlobal.service.AccessionActionService;
- import org.gringlobal.service.ActionService;
- import org.gringlobal.service.InventoryActionService;
- import org.gringlobal.service.InventoryViabilityActionService;
- import org.gringlobal.service.OrderRequestActionService;
- import org.gringlobal.service.OrderRequestItemActionService;
- import org.gringlobal.service.WorkflowService;
- import org.gringlobal.service.filter.WorkflowFilter;
- import org.gringlobal.spring.CustomStandardEvaluationContext;
- import org.hibernate.Hibernate;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.expression.Expression;
- import org.springframework.expression.ExpressionParser;
- import org.springframework.expression.spel.standard.SpelExpressionParser;
- import org.springframework.security.access.prepost.PreAuthorize;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
- import org.springframework.validation.annotation.Validated;
- import javax.validation.ValidationException;
- /**
- * The Class WorkflowServiceImpl.
- */
- @Service
- @Transactional(readOnly = true)
- @Validated
- @PreAuthorize("hasAuthority('GROUP_ADMINS')")
- @Slf4j
- public class WorkflowServiceImpl extends FilteredCRUDService2Impl<Workflow, WorkflowFilter, WorkflowRepository> implements WorkflowService {
- private final Cache<String, Expression> parsedExpressions = CacheBuilder.newBuilder().maximumSize(30).expireAfterAccess(10, TimeUnit.HOURS).build();
- @Autowired
- private WorkflowStepRepository workflowStepRepository;
- @Autowired
- private JPAQueryFactory jpaQueryFactory;
- private Map<String, ActionService> actionServices;
- private final ExpressionParser expressionParser = new SpelExpressionParser();
- private CustomStandardEvaluationContext context;
- @Override
- public void afterPropertiesSet() throws Exception {
- super.afterPropertiesSet();
- actionServices = new HashMap<>();
- actionServices.put(AccessionAction.class.getName(), CurrentApplicationContext.getContext().getBean(AccessionActionService.class));
- actionServices.put(InventoryAction.class.getName(), CurrentApplicationContext.getContext().getBean(InventoryActionService.class));
- actionServices.put(InventoryViabilityAction.class.getName(), CurrentApplicationContext.getContext().getBean(InventoryViabilityActionService.class));
- actionServices.put(OrderRequestAction.class.getName(), CurrentApplicationContext.getContext().getBean(OrderRequestActionService.class));
- actionServices.put(OrderRequestItemAction.class.getName(), CurrentApplicationContext.getContext().getBean(OrderRequestItemActionService.class));
- WorkflowHelperMethods.setJpaQueryFactory(jpaQueryFactory);
- context = new CustomStandardEvaluationContext();
- if (MapUtils.isNotEmpty(WorkflowHelperMethods.methods)) {
- WorkflowHelperMethods.methods.forEach(context::registerFunction);
- }
- }
- @Override
- public Workflow create(Workflow source) {
- return createFast(source);
- }
- @Override
- public Workflow createFast(Workflow source) {
- assert (CollectionUtils.isEmpty(source.getTransitions()));
- Workflow workflow = new Workflow();
- workflow.apply(source);
- if (!actionServices.containsKey(workflow.getActionType())) {
- throw new InvalidApiUsageException("Invalid workflow action type");
- }
- workflow = super.createFast(source);
- return createStartEndWorkflowSteps(workflow);
- }
- private Workflow createStartEndWorkflowSteps(Workflow workflow) {
- WorkflowStartStep startStep = new WorkflowStartStep();
- startStep.setWorkflow(workflow);
- WorkflowEndStep endStep = new WorkflowEndStep();
- endStep.setWorkflow(workflow);
- workflowStepRepository.saveAll(List.of(startStep, endStep));
- return repository.getReferenceById(workflow.getId());
- }
- @Override
- public Workflow update(Workflow updated, Workflow target) {
- return updateFast(updated, target);
- }
- @Override
- @Transactional
- public Workflow updateTransitions(Workflow target, List<WorkflowTransition> transitions) {
- var update = get(target.getId());
- if (CollectionUtils.isEmpty(transitions)) {
- update.getTransitions().clear();
- } else {
- validateWorkflowTransitions(transitions, update);
- if (update.getTransitions() == null) {
- update.setTransitions(transitions);
- } else {
- update.getTransitions().removeIf(t -> {
- boolean shouldRemove = /* By origin+target */
- null == transitions.stream()
- .filter(tt -> Objects.equals(tt.getOrigin().getId(), t.getOrigin().getId()) && Objects.equals(tt.getTarget().getId(), t.getTarget().getId()))
- // Find match in existing transitions
- .findFirst().orElse(null);
- // if (shouldRemove) {
- // log.warn("Removing {}", t);
- // } else {
- // log.warn("Keeping {}", t);
- // }
- return shouldRemove;
- });
- update.getTransitions().forEach(t -> {
- transitions.stream()
- .filter(tt ->
- Objects.equals(tt.getOrigin().getId(), t.getOrigin().getId())
- && Objects.equals(tt.getTarget().getId(), t.getTarget().getId())
- && !StringUtils.equals(tt.getCondition(), t.getCondition())
- )
- // Find match in existing transitions
- .findFirst()
- .ifPresent(updated -> t.setCondition(updated.getCondition()));
- });
- var toAdd = transitions.stream()
- // Add missing transitions
- .filter(t -> {
- boolean shouldAdd = /* By origin + target */
- null == update.getTransitions().stream()
- .filter(tt -> Objects.equals(tt.getOrigin().getId(), t.getOrigin().getId()) && Objects.equals(tt.getTarget().getId(), t.getTarget().getId()))
- // Fiund match in existing transitions
- .findFirst().orElse(null);
- // if (shouldAdd) {
- // log.warn("Adding {}", t);
- // } else {
- // log.warn("Ignoring {}", t);
- // }
- return shouldAdd;
- })
- // Collect
- .collect(Collectors.toList());
- update.getTransitions().addAll(toAdd);
- }
- }
- return repository.save(update);
- }
- @Override
- public void validateWorkflowTransitions(List<WorkflowTransition> transitions, Workflow workflow) {
- transitions.forEach(transition -> {
- assert (Objects.equals(transition.getOrigin().getWorkflow().getId(), workflow.getId()));
- assert (Objects.equals(transition.getTarget().getWorkflow().getId(), workflow.getId()));
- assert (!Objects.equals(transition.getOrigin().getId(), transition.getTarget().getId()));
- });
- var conditions = transitions.stream()
- .map(WorkflowTransition::getCondition)
- .filter(Objects::nonNull)
- .collect(Collectors.toSet());
- if (!conditions.isEmpty()) {
- for (String condition : conditions) {
- parseExpression(condition);
- }
- }
- // Check unlinked transitions
- var originSteps = transitions.stream()
- .map(WorkflowTransition::getOrigin)
- .filter(step -> Hibernate.unproxy(step) instanceof WorkflowActionStep)
- .collect(Collectors.toSet());
- var targetSteps = transitions.stream()
- .map(WorkflowTransition::getTarget)
- .filter(step -> Hibernate.unproxy(step) instanceof WorkflowActionStep)
- .collect(Collectors.toSet());
- for (WorkflowStep originStep : originSteps) {
- if (!targetSteps.contains(originStep)) {
- throw new InvalidApiUsageException("All steps must be present as origins and as targets in transitions");
- }
- }
- for (WorkflowStep targetStep : targetSteps) {
- if (!originSteps.contains(targetStep)) {
- throw new InvalidApiUsageException("All steps must be present as origins and as targets in transitions");
- }
- }
- var startTransitions = transitions.stream()
- .filter(transition -> Hibernate.unproxy(transition.getOrigin()) instanceof WorkflowStartStep)
- .collect(Collectors.toList());
- if (startTransitions.isEmpty() || startTransitions.stream().map(WorkflowTransition::getOrigin).distinct().count() > 1) {
- throw new InvalidApiUsageException("Transitions must have one workflow start step.");
- }
- checkStepsIsNotEquals(startTransitions);
- startTransitions.forEach(startTransition -> validateTransitions(transitions, startTransition.getTarget(), 0));
- }
- private Expression parseExpression(String condition) {
- try {
- return parsedExpressions.get("parsed_spel_expr:" + condition, () -> expressionParser.parseExpression(condition));
- } catch (Exception e) {
- throw new ValidationException("Invalid SpEL expression", e);
- }
- }
- private void validateTransitions(List<WorkflowTransition> transitions, WorkflowStep targetStep, int transitionNumber) {
- // Check if target is end step
- if ((Hibernate.unproxy(targetStep) instanceof WorkflowEndStep)) {
- return;
- }
- // The longest linked list of steps must be smaller than the transitions size.
- if (transitionNumber + 1 == transitions.size()) {
- if (!(Hibernate.unproxy(targetStep) instanceof WorkflowEndStep)) {
- // Cyclic links ?
- throw new InvalidApiUsageException("Cyclic transition links");
- }
- } else {
- // Find all transitions with origin = targetStep
- var targetTransitions = transitions.stream()
- .filter(transition -> transition.getOrigin().equals(targetStep))
- .collect(Collectors.toList());
- if (targetTransitions.size() < 1) {
- throw new InvalidApiUsageException("The flow chain is broken");
- }
- checkStepsIsNotEquals(targetTransitions);
- targetTransitions.forEach(transition -> validateTransitions(transitions, transition.getTarget(), transitionNumber + 1));
- }
- }
- private void checkStepsIsNotEquals(List<WorkflowTransition> transitions) {
- var targetSteps = transitions.stream()
- .filter(transition -> Hibernate.unproxy(transition.getTarget()) instanceof WorkflowActionStep)
- .map(transition -> (WorkflowActionStep) Hibernate.unproxy(transition.getTarget()))
- .collect(Collectors.toList());
- var distinctSteps = targetSteps.stream()
- .map(step -> String.valueOf(step.getActionAssignee().getId()).concat(step.getActionNameCode()))
- .distinct()
- .count();
- if (distinctSteps != targetSteps.size()) {
- throw new InvalidApiUsageException("Equal target steps are not allowed");
- }
- }
- @Override
- public Workflow updateFast(Workflow updated, Workflow target) {
- target.apply(updated); // Doesn't update List<WorkflowTransitions>
- return repository.save(target);
- }
- @Override
- public Workflow remove(Workflow entity) {
- return super.remove(entity);
- }
- @Transactional
- @Override
- public void createNextStepAction(AbstractAction<?> completedAction) {
- if (completedAction.getWorkflowStep() == null) {
- return;
- }
- var completedStep = workflowStepRepository.getReferenceById(completedAction.getWorkflowStep().getId());
- ActionService service = actionServices.get(completedStep.getWorkflow().getActionType()); // This will pull workflow from the database
- if (service != null) {
- var targetTransitions = jpaQueryFactory.select(QWorkflowTransition.workflowTransition).from(QWorkflowTransition.workflowTransition).where(QWorkflowTransition.workflowTransition.origin().eq(completedStep)).fetch();
- var targetSteps = targetTransitions.stream()
- .filter(transition -> {
- var condition = transition.getCondition();
- if (condition != null) {
- var action = service.get(completedAction.getId());
- return parseExpression(condition).getValue(new CustomStandardEvaluationContext(context, action), Boolean.class);
- }
- return true;
- })
- .map(WorkflowTransition::getTarget)
- .collect(Collectors.toList());
- targetSteps.stream()
- .map(nextStep -> Hibernate.unproxy(nextStep))
- .filter(nextStep -> nextStep instanceof WorkflowActionStep)
- .filter(nextStep -> !Objects.equals(nextStep, completedStep)) // Just in case!
- .forEach(nextStep -> {
- log.info("Completed step {} starting {}", completedStep, nextStep);
- service.createNextWorkflowStepAction((WorkflowActionStep) nextStep, completedAction);
- });
- }
- }
- @Override
- public <T extends AbstractAction<T>> List<T> startWorkflow(long workflowId, Set<Long> owningEntityIds, ActionCreator<T> actionCreator) {
- var loadedWorkflow = load(workflowId);
- var transitions = loadedWorkflow.getTransitions();
- var stepWithTransition = transitions.stream()
- .filter(transition -> Hibernate.unproxy(transition.getOrigin()) instanceof WorkflowStartStep && Hibernate.unproxy(transition.getTarget()) instanceof WorkflowActionStep)
- .collect(Collectors.toMap((t) -> (WorkflowActionStep)Hibernate.unproxy(t.getTarget()), (t) -> t));
-
- if (stepWithTransition.isEmpty()) {
- throw new IllegalArgumentException("The workflow does not contain steps to complete");
- }
-
- // Build actions by ActionCreator.buildActions
- var actions = actionCreator.buildActions(new ArrayList<>(stepWithTransition.keySet()));
-
- // Filter by condition and return to persist
- return actions.stream().filter(action -> {
- var transition = stepWithTransition.get(action.getWorkflowStep());
- var condition = transition.getCondition();
- if (condition != null) {
- return parseExpression(condition).getValue(new CustomStandardEvaluationContext(context, action), Boolean.class);
- }
- return true;
- }).collect(Collectors.toList());
- }
-
- public interface ActionCreator<T extends AbstractAction<T>> {
- List<T> buildActions(List<WorkflowActionStep> steps);
- }
- @Service
- @Transactional(readOnly = true)
- @Validated
- @PreAuthorize("hasAuthority('GROUP_ADMINS')")
- @Slf4j
- public static class WorkflowStepServiceImpl extends CRUDService2Impl<WorkflowStep, WorkflowStepRepository> implements WorkflowStepService {
- @Autowired
- private WorkflowRepository workflowRepository;
- @Autowired
- private WorkflowService workflowService;
- @Override
- @Transactional
- public WorkflowStep createFast(WorkflowStep source) {
- if (source instanceof WorkflowStartStep || source instanceof WorkflowEndStep) {
- throw new InvalidApiUsageException("WorkflowStartStep and WorkflowEndStep are automatically created along with the workflow.");
- }
- var workflow = workflowRepository.getReferenceById(source.getWorkflow().getId());
- String actionCodeGroup;
- try {
- Class<AbstractAction<?>> actionTypeClass = (Class<AbstractAction<?>>) Class.forName(workflow.getActionType());
- actionCodeGroup = actionTypeClass.getDeclaredConstructor().newInstance().getActionGroupName();
- } catch (Exception e) {
- throw new InvalidApiUsageException("Invalid workflow action type");
- }
- if (!AppContextHelper.validateCodeValue(actionCodeGroup, ((WorkflowActionStep) source).getActionNameCode())) {
- throw new InvalidApiUsageException("Invalid action code value");
- }
- return repository.save(source);
- }
- @Override
- @Transactional
- public WorkflowStep create(WorkflowStep source) {
- return createFast(source);
- }
- @Override
- @Transactional
- public WorkflowStep updateFast(WorkflowStep updated, WorkflowStep target) {
- target.apply(updated);
- var saved = repository.saveAndFlush(target);
- var workflow = workflowRepository.getReferenceById(saved.getWorkflow().getId());
- var transitions = workflow.getTransitions();
- if (transitions.stream().anyMatch(t -> t.getOrigin().equals(saved) || t.getTarget().equals(saved))) {
- workflowService.validateWorkflowTransitions(transitions, workflow);
- }
- return saved;
- }
- @Override
- @Transactional
- public WorkflowStep update(WorkflowStep updated, WorkflowStep target) {
- return updateFast(updated, target);
- }
- }
- }