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);
	}


}