ActionApiServiceImpl.java
/*
* Copyright 2026 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.api.v2.facade.impl;
import com.blazebit.persistence.CriteriaBuilderFactory;
import org.apache.commons.lang3.StringUtils;
import org.genesys.blocks.security.SecurityContextUtil;
import org.gringlobal.api.model.AbstractActionDTO;
import org.gringlobal.api.v2.facade.ActionApiService;
import org.gringlobal.api.v2.mapper.MapstructMapper;
import org.gringlobal.model.AbstractAction;
import org.gringlobal.model.AccessionAction;
import org.gringlobal.model.ActionRowCTE;
import org.gringlobal.model.InventoryAction;
import org.gringlobal.model.InventoryViabilityAction;
import org.gringlobal.model.OrderRequestAction;
import org.gringlobal.model.OrderRequestItemAction;
import org.gringlobal.model.SysUser;
import org.gringlobal.model.community.LocationAction;
import org.gringlobal.persistence.AccessionActionRepository;
import org.gringlobal.persistence.InventoryActionRepository;
import org.gringlobal.persistence.InventoryViabilityActionRepository;
import org.gringlobal.persistence.OrderRequestActionRepository;
import org.gringlobal.persistence.OrderRequestItemActionRepository;
import org.gringlobal.persistence.community.LocationActionRepository;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
@Service
public class ActionApiServiceImpl implements ActionApiService, InitializingBean {
@Autowired
private MapstructMapper mapper;
@Autowired
private CriteriaBuilderFactory criteriaBuilderFactory;
@Autowired
private EntityManager entityManager;
@Autowired
private AccessionActionRepository accessionActionRepository;
@Autowired
private InventoryActionRepository inventoryActionRepository;
@Autowired
private InventoryViabilityActionRepository inventoryViabilityActionRepository;
@Autowired
private LocationActionRepository locationActionRepository;
@Autowired
private OrderRequestActionRepository orderRequestActionRepository;
@Autowired
private OrderRequestItemActionRepository orderRequestItemActionRepository;
private static final Set<String> SORT_PROPERTIES = Set.of("createdDate", "startedDate", "notBeforeDate");
private Map<Class<? extends AbstractAction<?>>, Function<List<Long>, List<? extends AbstractAction<?>>>> actionClassFetchFunctionMap;
@Override
public void afterPropertiesSet() throws Exception {
actionClassFetchFunctionMap = new HashMap<>();
actionClassFetchFunctionMap.put(AccessionAction.class, accessionActionRepository::findAllById);
actionClassFetchFunctionMap.put(InventoryAction.class, inventoryActionRepository::findAllById);
actionClassFetchFunctionMap.put(InventoryViabilityAction.class, inventoryViabilityActionRepository::findAllById);
actionClassFetchFunctionMap.put(LocationAction.class, locationActionRepository::findAllById);
actionClassFetchFunctionMap.put(OrderRequestAction.class, orderRequestActionRepository::findAllById);
actionClassFetchFunctionMap.put(OrderRequestItemAction.class, orderRequestItemActionRepository::findAllById);
}
@Override
@Transactional(readOnly = true)
public Page<AbstractActionDTO> getMyActions(AbstractAction.ActionState state, Pageable page) {
Set<Sort.Order> sortOrders = new LinkedHashSet<>();
if (!page.getSort().isUnsorted()) {
var sort = page.getSort();
for (Sort.Order order : sort) {
if (!SORT_PROPERTIES.contains(order.getProperty())) {
throw new IllegalArgumentException("Sorting by '" + order.getProperty() + "' is not allowed");
}
sortOrders.add(order);
}
}
SysUser user = SecurityContextUtil.getCurrentUser();
Set<Long> assigneeSidIds = new HashSet<>();
assigneeSidIds.add(user.getId());
user.getGroupMaps().stream()
.map(groupMap -> groupMap.getSysGroup().getId()).forEach(assigneeSidIds::add);
var cb = criteriaBuilderFactory.create(entityManager, ActionRowCTE.class)
.with(ActionRowCTE.class, false)
// AccessionAction
.bind("id").select("id")
.bind("type").select("'" + AccessionAction.class.getName() + "'")
.bind("createdDate").select("createdDate")
.bind("startedDate").select("startedDate")
.bind("notBeforeDate").select("notBeforeDate")
.from(AccessionAction.class, "acceAction")
.where("acceAction.state").eq(state)
.where("acceAction.assignee.id").in(assigneeSidIds)
// InventoryAction
.union()
.bind("id").select("id")
.bind("type").select("'" + InventoryAction.class.getName() + "'")
.bind("createdDate").select("createdDate")
.bind("startedDate").select("startedDate")
.bind("notBeforeDate").select("notBeforeDate")
.from(InventoryAction.class, "invAction")
.where("invAction.state").eq(state)
.where("invAction.assignee.id").in(assigneeSidIds)
// InventoryViabilityAction
.union()
.bind("id").select("id")
.bind("type").select("'" + InventoryViabilityAction.class.getName() + "'")
.bind("createdDate").select("createdDate")
.bind("startedDate").select("startedDate")
.bind("notBeforeDate").select("notBeforeDate")
.from(InventoryViabilityAction.class, "invViabilityAction")
.where("invViabilityAction.state").eq(state)
.where("invViabilityAction.assignee.id").in(assigneeSidIds)
// LocationAction
.union()
.bind("id").select("id")
.bind("type").select("'" + LocationAction.class.getName() + "'")
.bind("createdDate").select("createdDate")
.bind("startedDate").select("startedDate")
.bind("notBeforeDate").select("notBeforeDate")
.from(LocationAction.class, "locAction")
.where("locAction.state").eq(state)
.where("locAction.assignee.id").in(assigneeSidIds)
// OrderRequestAction
.union()
.bind("id").select("id")
.bind("type").select("'" + OrderRequestAction.class.getName() + "'")
.bind("createdDate").select("createdDate")
.bind("startedDate").select("startedDate")
.bind("notBeforeDate").select("notBeforeDate")
.from(OrderRequestAction.class, "orAction")
.where("orAction.state").eq(state)
.where("orAction.assignee.id").in(assigneeSidIds)
// OrderRequestItemAction
.union()
.bind("id").select("id")
.bind("type").select("'" + OrderRequestItemAction.class.getName() + "'")
.bind("createdDate").select("createdDate")
.bind("startedDate").select("startedDate")
.bind("notBeforeDate").select("notBeforeDate")
.from(OrderRequestItemAction.class, "oriAction")
.where("oriAction.state").eq(state)
.where("oriAction.assignee.id").in(assigneeSidIds)
.endSet()
.end()
.selectNew(ActionRowCTE.class)
.with(0, "id")
.with(1, "type")
.with(2, "createdDate")
.with(3, "startedDate")
.with(4, "notBeforeDate")
.end()
.from(ActionRowCTE.class);
long totalItems = cb.getCountQuery().getSingleResult();
if (!sortOrders.isEmpty()) {
sortOrders.forEach(so -> cb.orderBy(so.getProperty(), so.isAscending()));
} else {
cb.orderByDesc("createdDate");
}
List<ActionRowCTE> rows = cb
.setFirstResult((int) page.getOffset())
.setMaxResults(page.getPageSize())
.getResultList();
Map<Class<? extends AbstractAction<?>>, List<Long>> actionClassToIdsMap = new HashMap<>();
Map<Pair<Long, Class<? extends AbstractAction<?>>>, Integer> orderIndex = new HashMap<>();
for (int i = 0; i < rows.size(); i++) {
try {
var row = rows.get(i);
// Group action types and ids
var actionType = (Class<? extends AbstractAction<?>>) Class.forName(StringUtils.trimToEmpty(row.getType()));
var ids = actionClassToIdsMap.computeIfAbsent(actionType, k -> new LinkedList<>());
ids.add(row.getId());
// Query response order
orderIndex.put(Pair.of(row.getId(), (Class<? extends AbstractAction<?>>) Class.forName(StringUtils.trimToEmpty(row.getType()))), i);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
// Fetch actual action entities
List<AbstractAction<?>> result = new LinkedList<>();
actionClassToIdsMap.forEach((actionClass, ids) -> {
result.addAll(actionClassFetchFunctionMap.get(actionClass).apply(ids));
});
// Sort by union query response order
result.sort(
Comparator.comparingInt(a ->
orderIndex.getOrDefault(
Pair.of(a.getId(), a.getClass()),
Integer.MAX_VALUE
)
)
);
return new PageImpl<>(mapper.map(result, mapper::map), page, totalItems);
}
}