BaseActionSupport.java
/*
* Copyright 2020 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 java.lang.reflect.ParameterizedType;
import java.time.Instant;
import java.time.Period;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.genesys.blocks.model.EmptyModel;
import org.genesys.blocks.model.EntityId;
import org.genesys.blocks.security.model.AclSid;
import org.genesys.blocks.security.persistence.AclSidPersistence;
import org.gringlobal.api.exception.InvalidApiUsageException;
import org.gringlobal.model.AbstractAction;
import org.gringlobal.model.Cooperator;
import org.gringlobal.model.CooperatorOwnedModel;
import org.gringlobal.model.QAbstractAction;
import org.gringlobal.model.community.CommunityCodeValues;
import org.gringlobal.model.workflow.WorkflowActionStep;
import org.gringlobal.model.workflow.WorkflowStartStep;
import org.gringlobal.spring.persistence.ExtendedJpaRepository;
import org.hibernate.Hibernate;
import org.gringlobal.service.ActionService;
import org.gringlobal.service.ActionService.ActionRequest;
import org.gringlobal.service.ActionService.ActionScheduleFilter;
import org.gringlobal.service.CooperatorService;
import org.gringlobal.service.WorkflowService;
import org.gringlobal.service.filter.ActionFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.ExpressionUtils;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.DateTimePath;
import com.querydsl.core.types.dsl.NumberPath;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.core.types.dsl.StringPath;
import com.querydsl.jpa.impl.JPAQuery;
import javax.validation.constraints.NotNull;
/**
* The BaseActionSupport implementation of {@link ActionService}.
*
* @author Matija Obreza
* @author Maxym Borodenko
*/
@Validated
@Slf4j
public abstract class BaseActionSupport<O extends CooperatorOwnedModel, T extends AbstractAction<T>, F extends ActionFilter<F, T>, P extends ExtendedJpaRepository<T, Long>, R extends ActionRequest, ASF extends ActionScheduleFilter<ASF, T>>
extends CRUDService2Impl<T, P> implements ActionService<T, F, R, ASF> {
protected static final int OWNING_TYPE_GENERIC_INDEX = 0;
protected static final int ACTION_TYPE_GENERIC_INDEX = 1;
@SuppressWarnings("unchecked")
private final Class<T> actionType = ((Class<T>)((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[ACTION_TYPE_GENERIC_INDEX]);
private final NumberPath<Long> qActionOwningEntityId;
private final StringPath qActionNameCode;
private final DateTimePath<Instant> qStartedDate;
private final DateTimePath<Instant> qCompletedDate;
private final DateTimePath<Instant> qNotBeforeDate;
private final DateTimePath<Instant> qCreatedDate;
private final StringPath qIsDone;
// private final EnumPath<AbstractAction.ActionState> qState;
private final PathBuilder<AclSid> qAssignee;
private final PathBuilder<T> actionEntityPathBuilder;
private final PathBuilder<O> owningEntityPathBuilder;
private final PathBuilder<Cooperator> cooperatorEntityPathBuilder;
@Autowired
protected ExtendedJpaRepository<T, Long> actionRepository;
@Autowired
protected QuerydslPredicateExecutor<T> actionFinder;
@Autowired
private CooperatorService cooperatorService;
@Autowired
private AclSidPersistence aclSidPersistence;
@Autowired
@Lazy
private WorkflowService workflowService;
@SuppressWarnings("unchecked")
public BaseActionSupport() {
Class<O> owningEntityType = ((Class<O>)((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[OWNING_TYPE_GENERIC_INDEX]);
Class<T> actionEntityType = ((Class<T>)((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[ACTION_TYPE_GENERIC_INDEX]);
actionEntityPathBuilder = new PathBuilder<>(actionEntityType, toVariable(actionEntityType));
owningEntityPathBuilder = actionEntityPathBuilder.get(getOwningEntityPath().getMetadata().getName(), owningEntityType);
cooperatorEntityPathBuilder = actionEntityPathBuilder.get(QAbstractAction.abstractAction.cooperator().getMetadata().getName(), Cooperator.class);
qActionOwningEntityId = owningEntityPathBuilder.getNumber("id", Long.class);
qAssignee = actionEntityPathBuilder.get(QAbstractAction.abstractAction.assignee().getMetadata().getName(), AclSid.class);
qCompletedDate = actionEntityPathBuilder.getDateTime(QAbstractAction.abstractAction.completedDate.getMetadata().getName(), Instant.class);
qStartedDate = actionEntityPathBuilder.getDateTime(QAbstractAction.abstractAction.startedDate.getMetadata().getName(), Instant.class);
qNotBeforeDate = actionEntityPathBuilder.getDateTime(QAbstractAction.abstractAction.notBeforeDate.getMetadata().getName(), Instant.class);
qCreatedDate = actionEntityPathBuilder.getDateTime(QAbstractAction.abstractAction.createdDate.getMetadata().getName(), Instant.class);
qActionNameCode = actionEntityPathBuilder.getString(QAbstractAction.abstractAction.actionNameCode.getMetadata().getName());
qIsDone = actionEntityPathBuilder.getString(QAbstractAction.abstractAction.isDone.getMetadata().getName());
// qState = actionEntityPathBuilder.getEnum(QAbstractAction.abstractAction.createdDate.getMetadata().getName(), AbstractAction.ActionState.class);
}
protected abstract EntityPath<O> getOwningEntityPath();
@Override
public Class<T> getActionType() {
return actionType;
}
private String toVariable(Class<T> clazz) {
String simpleName = clazz.getSimpleName();
return Character.toLowerCase(simpleName.charAt(0)) + simpleName.substring(1);
}
@Override
@Transactional
public List<T> scheduleAction(R actionData) {
if (actionData == null || CollectionUtils.isEmpty(actionData.id) || StringUtils.isBlank(actionData.actionNameCode)) {
throw new InvalidApiUsageException();
}
Set<Long> entityIds = new HashSet<>(actionData.id);
BooleanExpression booleanExpression = qActionOwningEntityId.in(entityIds)
.and(qActionNameCode.eq(actionData.actionNameCode))
.and(qCompletedDate.isNull());
// find existing pending and open actions for provided items, but don't reset start date
List<T> existingActions = Lists.newArrayList(actionFinder.findAll(booleanExpression)).stream().map(a -> {
// remove id from the set
entityIds.remove(a.getOwningEntityId());
// Don't change existing action
return a;
}).collect(Collectors.toList());
List<T> newActions = Lists.newArrayList(findOwningEntities(entityIds)).stream().map(entity -> {
T action = createAction(entity);
if (actionData != null) {
updateAction(action, actionData);
}
action.setActionNameCode(actionData.actionNameCode);
if (actionData.note != null) {
action.setNote(actionData.note);
}
if (actionData.cooperator != null && !actionData.cooperator.isNew()) {
action.setCooperator(cooperatorService.get(actionData.cooperator.getId()));
} else {
action.setCooperator(null);
}
if (actionData.assignee != null && !actionData.assignee.isNew()) {
action.setAssignee(aclSidPersistence.getReferenceById(actionData.assignee.getId()));
}
action.setNotBeforeDate(actionData.notBeforeDate);
action.setNotBeforeDateCode(actionData.notBeforeDateCode);
action.setStartedDate(null); // no start date
action.setCompletedDate(null); // no end date
return create(action);
}).collect(Collectors.toList());
ArrayList<T> allActions = new ArrayList<>(existingActions);
allActions.addAll(newActions);
return allActions;
}
@Override
@Transactional
public List<T> startAction(R actionData) {
if (actionData == null || CollectionUtils.isEmpty(actionData.id) || StringUtils.isBlank(actionData.actionNameCode)) {
throw new InvalidApiUsageException();
}
Set<Long> entityIds = new HashSet<>(actionData.id);
BooleanExpression booleanExpression = qActionOwningEntityId.in(entityIds)
.and(qActionNameCode.eq(actionData.actionNameCode))
.and(qCompletedDate.isNull());
// find existing open actions for provided items and reset start date
List<T> existingActions = Lists.newArrayList(actionFinder.findAll(booleanExpression)).stream().map(a -> {
// remove id from the set
entityIds.remove(a.getOwningEntityId());
// reset started date
a.setStartedDate(Instant.now());
a.setStartedDateCode(CommunityCodeValues.DATE_FORMAT_DATETIME.value);
return update(a);
}).collect(Collectors.toList());
List<T> newActions = StreamSupport.stream(findOwningEntities(entityIds).spliterator(), false).map(owningEntity -> {
T action = createAction(owningEntity);
if (actionData != null) {
updateAction(action, actionData);
}
action.setActionNameCode(actionData.actionNameCode);
if (actionData.note != null) {
action.setNote(actionData.note);
}
if (actionData.cooperator != null && !actionData.cooperator.isNew()) {
action.setCooperator(cooperatorService.get(actionData.cooperator.getId()));
} else {
action.setCooperator(null);
}
if (actionData.assignee != null && !actionData.assignee.isNew()) {
action.setAssignee(aclSidPersistence.getReferenceById(actionData.assignee.getId()));
}
action.setStartedDate(Instant.now());
action.setStartedDateCode(CommunityCodeValues.DATE_FORMAT_DATETIME.value);
action.setCompletedDate(null);
action.setCompletedDateCode(null);
return create(action);
}).collect(Collectors.toList());
List<T> allActions = new LinkedList<>(existingActions);
allActions.addAll(newActions);
return allActions;
}
protected abstract T createAction(O owningEntity);
protected abstract void updateAction(T action, R actionData);
protected abstract Iterable<O> findOwningEntities(Set<Long> id);
@Override
@Transactional
public List<T> completeAction(R actionData) {
if (actionData == null || CollectionUtils.isEmpty(actionData.id) || StringUtils.isBlank(actionData.actionNameCode)) {
throw new InvalidApiUsageException();
}
BooleanExpression expression = qActionOwningEntityId.in(actionData.id)
.and(qStartedDate.isNotNull())
.and(qCompletedDate.isNull())
.and(qActionNameCode.eq(actionData.actionNameCode));
// Keep track of unique items
Set<Long> remainingIds = new HashSet<>(actionData.id);
List<T> actions = Lists.newArrayList(actionFinder.findAll(expression)).stream().map(action -> {
if (!remainingIds.contains(action.getOwningEntityId())) {
return null;
}
// updateAction(action, actionData); // DO NOT UPDATE OTHER DATA
action.setCompletedDate(Instant.now());
action.setIsDone("Y");
action.setCompletedDateCode(CommunityCodeValues.DATE_FORMAT_DATETIME.value);
if (actionData.note != null) {
action.setNote(actionData.note);
}
if (actionData.cooperator != null && !actionData.cooperator.isNew()) {
action.setCooperator(cooperatorService.get(actionData.cooperator.getId()));
}
remainingIds.remove(action.getOwningEntityId());
return update(action);
}).filter(Objects::nonNull).collect(Collectors.toList());
if (remainingIds.size() > 0) {
throw new InvalidApiUsageException("Some actions could not be completed");
}
return actions;
}
@Override
@Transactional
public List<T> cancelAction(R actionData) {
if (actionData == null || CollectionUtils.isEmpty(actionData.id) || StringUtils.isBlank(actionData.actionNameCode)) {
throw new InvalidApiUsageException();
}
BooleanExpression expression = qActionOwningEntityId.in(actionData.id)
.and(qCompletedDate.isNull())
.and(qIsDone.isNull())
.and(qActionNameCode.eq(actionData.actionNameCode));
// Keep track of unique items
Set<Long> remainingIds = new HashSet<>(actionData.id);
List<T> actions = Lists.newArrayList(actionFinder.findAll(expression)).stream().map(action -> {
if (!remainingIds.contains(action.getOwningEntityId())) {
return null;
}
action.setCompletedDate(Instant.now());
action.setIsDone("N");
action.setCompletedDateCode(CommunityCodeValues.DATE_FORMAT_DATETIME.value);
if (actionData.note != null) {
action.setNote(actionData.note);
}
if (actionData.cooperator != null && !actionData.cooperator.isNew()) {
action.setCooperator(cooperatorService.get(actionData.cooperator.getId()));
}
remainingIds.remove(action.getOwningEntityId());
return update(action);
}).filter(Objects::nonNull).collect(Collectors.toList());
if (remainingIds.size() > 0) {
throw new InvalidApiUsageException("Some actions could not be canceled");
}
return actions;
}
@Override
@Transactional
public List<T> reopenAction(R actionData) {
if (actionData == null || CollectionUtils.isEmpty(actionData.id) || StringUtils.isBlank(actionData.actionNameCode)) {
throw new InvalidApiUsageException();
}
// Keep track of unique items
Set<Long> remainingIds = new HashSet<>(actionData.id);
BooleanExpression expression = qActionOwningEntityId.in(actionData.id)
.and(qStartedDate.isNotNull())
.and(qCompletedDate.isNotNull())
.and(qActionNameCode.eq(actionData.actionNameCode));
List<T> latestActions = new ArrayList<>();
actionFinder.findAll(expression, qCompletedDate.desc()).forEach(action -> {
if (latestActions.stream().noneMatch(a -> a.getOwningEntityId().equals(action.getOwningEntityId()))) {
latestActions.add(action);
remainingIds.remove(action.getOwningEntityId());
}
});
if (remainingIds.size() > 0) {
throw new InvalidApiUsageException("Some actions could not be reopened.");
}
return latestActions.stream().map(action -> {
// updateAction(action, actionData); // DO NOT UPDATE OTHER DATA
action.setCompletedDate(null);
action.setCompletedDateCode(null);
action.setIsDone(null);
action.setStartedDate(Instant.now());
action.setStartedDateCode(CommunityCodeValues.DATE_FORMAT_DATETIME.value);
if (actionData.note != null) {
action.setNote(actionData.note);
}
return actionRepository.save(action);
}).collect(Collectors.toList());
}
@Override
@Transactional
public T update(T updated, T target) {
target.apply(updated);
log.debug("Update action. Input data {}", updated);
T saved = actionRepository.save(target);
saved.lazyLoad();
return saved;
}
@Override
@Transactional
public T updateFast(T updated, T target) {
target.apply(updated);
return actionRepository.save(target);
}
@Override
@Transactional(readOnly = true)
public Page<T> listActions(F filter, Pageable page) {
var result = actionFinder.findAll(filter.buildPredicate(), page);
initializeActionDetails(result.getContent());
return result;
}
@Override
@Transactional(readOnly = true)
public long countActions(F filter) {
return actionFinder.count(filter.buildPredicate());
}
@Override
public Page<T> listCompletedActions(ASF filter, Pageable page) {
return actionRepository.findAll(buildQueryCompleted(filter)
// fetch join owning entity
.join(owningEntityPathBuilder).fetchJoin()
// fetch join cooperator
.leftJoin(cooperatorEntityPathBuilder).fetchJoin(), page);
}
@Override
public Page<T> listCanceledActions(ASF filter, Pageable page) {
return actionRepository.findAll(buildQueryCanceled(filter)
// fetch join owning entity
.join(owningEntityPathBuilder).fetchJoin()
// fetch join cooperator
.leftJoin(cooperatorEntityPathBuilder).fetchJoin(), page);
}
@Override
public Page<T> listInProgressActions(ASF filter, Pageable page) {
return actionRepository.findAll(buildQueryInProgress(filter)
// fetch join owning entity
.join(owningEntityPathBuilder).fetchJoin()
// fetch join cooperator
.leftJoin(cooperatorEntityPathBuilder).fetchJoin(), page);
}
@Override
public List<T> listInProgressActions(R actionData) {
BooleanExpression expression = qActionOwningEntityId.in(actionData.id)
.and(qStartedDate.isNotNull())
.and(qCompletedDate.isNull())
.and(qActionNameCode.eq(actionData.actionNameCode));
return Lists.newArrayList(actionFinder.findAll(expression));
}
@Override
public Page<T> listScheduledActions(ASF filter, Pageable page) {
return actionRepository.findAll(buildQueryScheduled(filter)
// fetch join owning entity
.join(owningEntityPathBuilder).fetchJoin()
// fetch join cooperator
.leftJoin(cooperatorEntityPathBuilder).fetchJoin(), page);
}
@Override
public Page<T> listAddedActions(ASF filter, Pageable page) {
return actionRepository.findAll(buildQueryAdded(filter)
// fetch join owning entity
.join(owningEntityPathBuilder).fetchJoin()
// fetch join cooperator
.leftJoin(cooperatorEntityPathBuilder).fetchJoin(), page);
}
/**
* Override to initialize action details. Example:
*
* <pre>
* actions.forEach(action -> {
* Hibernate.initialize(action.getInventory());
* Hibernate.initialize(action.getInventory().getAccession());
* });
* </pre>
*
* @param actions
*/
protected abstract void initializeActionDetails(List<T> actions);
@Override
public Page<T> listCreatedActions(ASF filter, Pageable page) {
return actionRepository.findAll(buildQueryCreated(filter)
// fetch join owning entity
.join(owningEntityPathBuilder).fetchJoin()
// fetch join cooperator
.leftJoin(cooperatorEntityPathBuilder).fetchJoin(), page);
}
@Override
public Page<T> listOverdueActions(ASF filter, Pageable page) {
return actionRepository.findAll(buildQueryOverdue(filter)
// fetch join owning entity
.join(owningEntityPathBuilder).fetchJoin()
// fetch join cooperator
.leftJoin(cooperatorEntityPathBuilder).fetchJoin(), page);
}
@Override
public ActionScheduleOverview actionScheduleOverview(ASF filter) {
NumberPath<Long> qActionId = actionEntityPathBuilder.getNumber("id", Long.class);
// Completed
JPAQuery<OverviewRow> completedQuery = buildQueryCompleted(filter)
.select(Projections.constructor(OverviewRow.class, qActionNameCode, qActionId.countDistinct())).groupBy(qActionNameCode);
// Canceled
JPAQuery<OverviewRow> canceledQuery = buildQueryCanceled(filter)
.select(Projections.constructor(OverviewRow.class, qActionNameCode, qActionId.countDistinct())).groupBy(qActionNameCode);
// In progress
JPAQuery<OverviewRow> inProgressQuery = buildQueryInProgress(filter)
.select(Projections.constructor(OverviewRow.class, qActionNameCode, qActionId.countDistinct())).groupBy(qActionNameCode);
// Scheduled
JPAQuery<OverviewRow> scheduledQuery = buildQueryScheduled(filter)
.select(Projections.constructor(OverviewRow.class, qActionNameCode, qActionId.countDistinct())).groupBy(qActionNameCode);
// New, but unscheduled actions
JPAQuery<OverviewRow> addedQuery = buildQueryAdded(filter)
.select(Projections.constructor(OverviewRow.class, qActionNameCode, qActionId.countDistinct())).groupBy(qActionNameCode);
// Created
JPAQuery<OverviewRow> createdQuery = buildQueryCreated(filter)
.select(Projections.constructor(OverviewRow.class, qActionNameCode, qActionId.countDistinct())).groupBy(qActionNameCode);
// Overdue
JPAQuery<OverviewRow> overdueQuery = buildQueryOverdue(filter)
.select(Projections.constructor(OverviewRow.class, qActionNameCode, qActionId.countDistinct())).groupBy(qActionNameCode);
var overview = new ActionScheduleOverview();
overview.completed = completedQuery.fetch().stream().collect(Collectors.toMap(OverviewRow::getActionName, OverviewRow::getCount));
overview.canceled = canceledQuery.fetch().stream().collect(Collectors.toMap(OverviewRow::getActionName, OverviewRow::getCount));
overview.inProgress = inProgressQuery.fetch().stream().collect(Collectors.toMap(OverviewRow::getActionName, OverviewRow::getCount));
overview.scheduled = scheduledQuery.fetch().stream().collect(Collectors.toMap(OverviewRow::getActionName, OverviewRow::getCount));
overview.added = addedQuery.fetch().stream().collect(Collectors.toMap(OverviewRow::getActionName, OverviewRow::getCount));
overview.created = createdQuery.fetch().stream().collect(Collectors.toMap(OverviewRow::getActionName, OverviewRow::getCount));
overview.overdue = overdueQuery.fetch().stream().collect(Collectors.toMap(OverviewRow::getActionName, OverviewRow::getCount));
return overview;
}
@Override
@Transactional
public List<T> assignActions(@NotNull Map<Long, String> actionAssigneeMap) {
if (actionAssigneeMap.isEmpty()) {
return new ArrayList<>();
}
var actions = actionRepository
.findAllById(actionAssigneeMap.keySet())
.stream().collect(Collectors.toMap(EntityId::getId, action -> action));
var sidIds = actionAssigneeMap.values().stream()
.filter(Objects::nonNull)
.map(aclSidPersistence::getSidId)
.collect(Collectors.toSet());
var sids = aclSidPersistence
.findAllById(sidIds)
.stream().collect(Collectors.toMap(AclSid::getSid, sid -> sid));
var actionsToSave = new ArrayList<T>();
for (var actionAssignee : actionAssigneeMap.entrySet()) {
var actionId = actionAssignee.getKey();
var sidId = actionAssignee.getValue();
var action = actions.get(actionId);
// Check if action or sid(if not null from request) not found
if (action == null || (sidId != null && !sids.containsKey(sidId))) {
continue;
}
action.setAssignee(sidId == null ? null : sids.get(sidId));
actionsToSave.add(action);
}
return actionRepository.saveAllAndFlush(actionsToSave);
}
@Override
public List<T> startWorkflow(long workflowId, Set<Long> owningEntityIds) {
if (CollectionUtils.isEmpty(owningEntityIds)) {
return List.of();
}
var newActions = workflowService.startWorkflow(workflowId, owningEntityIds, (steps) ->
StreamSupport.stream(findOwningEntities(owningEntityIds).spliterator(), false)
.sorted(Ordering.explicit(new LinkedList<>(owningEntityIds)).onResultOf(EmptyModel::getId)) // Sort by incoming order
.flatMap(owningEntity -> steps.stream().map(step -> {
T action = createAction(owningEntity);
action.setWorkflowStep(step);
action.setActionNameCode(step.getActionNameCode());
action.setAssignee(step.getActionAssignee());
return action;
}))
.collect(Collectors.toList())
);
return actionRepository.saveAll(newActions);
}
public final T createNextWorkflowStepAction(WorkflowActionStep nextStep, T completedAction) {
T nextAction = prepareNextWorkflowStepAction(nextStep, completedAction);
// Same logic for all types
nextAction.setWorkflowStep(nextStep);
nextAction.setActionNameCode(nextStep.getActionNameCode());
nextAction.setAssignee(nextStep.getActionAssignee());
if (nextStep.getDelay() != null) {
nextAction.setNotBeforeDate(ZonedDateTime.now().plus(Period.parse(nextStep.getDelay())).toInstant());
nextAction.setNotBeforeDateCode(CommunityCodeValues.DATE_FORMAT_DATETIME.value);
}
return actionRepository.save(nextAction);
}
/**
* Override to support Workflow execution
*
*/
protected abstract T prepareNextWorkflowStepAction(WorkflowActionStep nextStep, T completedAction);
protected static class OverviewRow {
public String actionName;
public Number count;
public OverviewRow(String actionCodeName, Number count) {
this.actionName = actionCodeName;
this.count = count;
}
public String getActionName() {
return actionName;
}
public Number getCount() {
return count;
}
}
private JPAQuery<T> buildQueryCompleted(ASF filter) {
// completedDate >= DATE1 and completedDate < DATE2
var predicate = ExpressionUtils.allOf(
qCompletedDate.goe(filter.fromInclusive).and(qCompletedDate.lt(filter.toExclusive)),
qIsDone.eq("Y") // qState.eq(ActionState.COMPLETED)
);
return buildQuery(filter, predicate);
}
private JPAQuery<T> buildQueryCanceled(ASF filter) {
// completedDate >= DATE1 and completedDate < DATE2 and isDone == "N"
var predicate = ExpressionUtils.allOf(
qCompletedDate.goe(filter.fromInclusive).and(qCompletedDate.lt(filter.toExclusive)),
qIsDone.eq("N") // qState.eq(ActionState.CANCELED)
);
return buildQuery(filter, predicate);
}
private JPAQuery<T> buildQueryInProgress(ASF filter) {
BooleanBuilder predicate = new BooleanBuilder();
// startedDate < DATE2 and (completedDate is null or completedDate > DATE2)
predicate.and(qStartedDate.before(filter.toExclusive)
.andAnyOf(
qCompletedDate.isNull(),
qCompletedDate.gt(filter.toExclusive)
)
);
return buildQuery(filter, predicate);
}
private JPAQuery<T> buildQueryScheduled(ASF filter) {
// notBeforeDate >= DATE1 and notBeforeDate < DATE2
var predicate = qNotBeforeDate.goe(filter.fromInclusive).and(qNotBeforeDate.lt(filter.toExclusive));
return buildQuery(filter, predicate);
}
private JPAQuery<T> buildQueryAdded(ASF filter) {
// notBeforeDate IS NULL and createdDate >= DATE1 and createdDate < DATE2
var predicate = qNotBeforeDate.isNull().and(qCreatedDate.goe(filter.fromInclusive).and(qCreatedDate.lt(filter.toExclusive)));
return buildQuery(filter, predicate);
}
private JPAQuery<T> buildQueryCreated(ASF filter) {
// createdDate >= DATE1 and createdDate < DATE2
var predicate = qCreatedDate.goe(filter.fromInclusive).and(qCreatedDate.lt(filter.toExclusive));
return buildQuery(filter, predicate);
}
private JPAQuery<T> buildQueryOverdue(ASF filter) {
// startedDate IS NULL and completedDate IS NULL and ((notBeforeDate IS NULL and createdDate < DATE2) or (notBeforeDate < DATE2))
var predicate = qStartedDate.isNull().and(qCompletedDate.isNull()).andAnyOf(
// notBeforeDate IS NULL and createdDate < DATE2
qNotBeforeDate.isNull().and(qCreatedDate.lt(filter.toExclusive)),
// notBeforeDate < DATE2
qNotBeforeDate.lt(filter.toExclusive));
return buildQuery(filter, predicate);
}
private JPAQuery<T> buildQuery(ASF filter, Predicate datePredicate) {
assert datePredicate != null;
List<Predicate> predicates = new ArrayList<>();
// set date predicate
predicates.add(datePredicate);
predicates.addAll(buildPredicates(filter));
var predicate = ExpressionUtils.allOf(predicates);
if (filter.NOT != null) {
var notPredicates = buildPredicates(filter.NOT);
if (!notPredicates.isEmpty()) {
var notAny = ExpressionUtils.anyOf(notPredicates);
if (notAny != null) predicate = ExpressionUtils.and(predicate, notAny.not());
}
}
if (filter.AND != null) {
var andPredicates = buildPredicates(filter.AND);
if (!andPredicates.isEmpty()) predicate = ExpressionUtils.and(predicate, ExpressionUtils.allOf(andPredicates));
}
if (filter.OR != null) {
var orPredicates = buildPredicates(filter.OR);
if (!orPredicates.isEmpty()) {
orPredicates.add(datePredicate);
predicate = ExpressionUtils.or(predicate, ExpressionUtils.allOf(orPredicates));
}
}
return jpaQueryFactory.selectFrom(actionEntityPathBuilder)
// avoid cross join
.join(owningEntityPathBuilder)
// apply predicates
.where(predicate);
}
private List<Predicate> buildPredicates(ASF filter) {
List<Predicate> predicates = new ArrayList<>();
applyOwningEntityFilter(filter, owningEntityPathBuilder.toString(), predicates);
if (CollectionUtils.isNotEmpty(filter.actionCode)) {
BooleanBuilder predicate = new BooleanBuilder();
predicate.and(qActionNameCode.in(filter.actionCode));
predicates.add(predicate);
}
if (filter.assignee != null && filter.assignee.sid != null && CollectionUtils.isNotEmpty(filter.assignee.sid.eq)) {
predicates.add(qAssignee.getString("sid").in(filter.assignee.sid.eq));
}
return predicates;
}
protected abstract void applyOwningEntityFilter(ASF filter, String alias, List<Predicate> predicates);
}