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.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
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.spring.persistence.ExtendedJpaRepository;
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.filter.ActionFilter;
import org.springframework.beans.factory.annotation.Autowired;
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.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<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;
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 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;
@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);
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());
}
protected abstract EntityPath<O> getOwningEntityPath();
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, actionData);
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 = Lists.newArrayList(findOwningEntities(entityIds)).stream().map(orderRequestItem -> {
T action = createAction(orderRequestItem, actionData);
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());
ArrayList<T> allActions = new ArrayList<>(existingActions);
allActions.addAll(newActions);
return allActions;
}
protected abstract T createAction(O owningEntity, R actionData);
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.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> 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.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> 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);
// 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.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, Long> actionAssigneeMap) {
if (actionAssigneeMap.isEmpty()) {
return new ArrayList<>();
}
var actions = actionRepository
.findAllById(actionAssigneeMap.keySet())
.stream().collect(Collectors.toMap(EntityId::getId, action -> action));
var sids = aclSidPersistence
.findAllById(actionAssigneeMap.values().stream().filter(Objects::nonNull).collect(Collectors.toSet()))
.stream().collect(Collectors.toMap(AclSid::getId, 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);
}
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) {
BooleanBuilder predicate = new BooleanBuilder();
// completedDate >= DATE1 and completedDate < DATE2
predicate.and(
qCompletedDate.goe(filter.fromInclusive).and(qCompletedDate.lt(filter.toExclusive))
);
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);
applyOwningEntityFilter(filter, owningEntityPathBuilder.toString(), predicates);
if (CollectionUtils.isNotEmpty(filter.actionCode)) {
BooleanBuilder predicate = new BooleanBuilder();
predicate.and(qActionNameCode.in(filter.actionCode));
predicates.add(predicate);
}
return jpaQueryFactory.selectFrom(actionEntityPathBuilder)
// avoid cross join
.join(owningEntityPathBuilder)
// apply predicates
.where(ExpressionUtils.allOf(predicates));
}
protected abstract void applyOwningEntityFilter(ASF filter, String alias, List<Predicate> predicates);
}