GeographyTriggers.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.service.triggers;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.CaseBuilder;
import com.querydsl.core.types.dsl.NumberExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.gringlobal.model.Geography;
import org.gringlobal.model.GeographyRegionMap;
import org.gringlobal.model.QGeography;
import org.gringlobal.model.QGeographyRegionMap;
import org.gringlobal.persistence.GeographyRegionMapRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

@Aspect
@Component("GeographyTriggers")
@Slf4j
public class GeographyTriggers {

	@Autowired
	private JPAQueryFactory jpaQueryFactory;

	@Autowired
	private GeographyRegionMapRepository geographyRegionMapRepository;

	@Pointcut("execution(* org.gringlobal.persistence.GeographyRepository.save(..)) || execution(* org.gringlobal.persistence.GeographyRepository.saveAndFlush(..))")
	public void saveGeography() {
	}

	@Pointcut("execution(* org.gringlobal.persistence.GeographyRepository.saveAll(Iterable)) || execution(* org.gringlobal.persistence.GeographyRepository.saveAllAndFlush(Iterable))")
	public void saveGeographies() {
	}

	@Pointcut("execution(* org.gringlobal.persistence.GeographyRepository.delete(..)) || execution(* org.gringlobal.persistence.GeographyRepository.deleteAll(..))")
	public void removeGeographies() {
	}

	@Pointcut("execution(* org.gringlobal.persistence.GeographyRegionMapRepository.delete(..)) || execution(* org.gringlobal.persistence.GeographyRegionMapRepository.deleteAll(..))")
	public void removeGeographyRegionMappings() {
	}

	@After(value = "saveGeography() || saveGeographies()")
	public void afterGeographiesSave(final JoinPoint joinPoint) {
		var result = joinPoint.getArgs()[0];
		if (result instanceof Collection) {
			((Collection<Geography>) result).forEach(this::assignGeographyRegionIfExists);
		} else {
			assignGeographyRegionIfExists((Geography) result);
		}
	}

	private void assignGeographyRegionIfExists(Geography geography) {

		var qGeography = QGeography.geography;
		var qGeographyRegionMap = QGeographyRegionMap.geographyRegionMap;

		BooleanExpression base = qGeography.countryCode.eq(geography.getCountryCode());
		List<BooleanExpression> levels = new ArrayList<>();
		if (geography.getAdm4() != null) {
			levels.add(base
				.and(qGeography.adm1.eq(geography.getAdm1())).and(qGeography.adm2.eq(geography.getAdm2()))
				.and(qGeography.adm3.eq(geography.getAdm3())).and(qGeography.adm4.isNull()));
		}
		if (geography.getAdm3() != null) {
			levels.add(base
				.and(qGeography.adm1.eq(geography.getAdm1())).and(qGeography.adm2.eq(geography.getAdm2()))
				.and(qGeography.adm3.isNull()).and(qGeography.adm4.isNull()));
		}
		if (geography.getAdm2() != null) {
			levels.add(base
				.and(qGeography.adm1.eq(geography.getAdm1())).and(qGeography.adm2.isNull())
				.and(qGeography.adm3.isNull()).and(qGeography.adm4.isNull()));
		}
		if (geography.getAdm1() != null) {
			levels.add(base
				.and(qGeography.adm1.isNull()).and(qGeography.adm2.isNull())
				.and(qGeography.adm3.isNull()).and(qGeography.adm4.isNull()));
		}
		NumberExpression<Integer> levelOrder = new CaseBuilder()
			// parent of adm4
			.when(qGeography.adm4.isNull().and(qGeography.adm3.isNotNull())).then(3)
			// parent of adm3
			.when(qGeography.adm3.isNull().and(qGeography.adm2.isNotNull())).then(2)
			// parent of adm2
			.when(qGeography.adm2.isNull().and(qGeography.adm1.isNotNull())).then(1)
			// country level
			.otherwise(0);

		if (levels.isEmpty()) {
			return; // country level
		}
		BooleanExpression levelsExpression = levels.stream().reduce(BooleanExpression::or).orElse(null);

		var region = jpaQueryFactory
			.select(qGeographyRegionMap.region()).from(qGeographyRegionMap)
			.join(qGeographyRegionMap.geography(), qGeography)
			.where(levelsExpression)
			.orderBy(levelOrder.desc())
			.fetchFirst();

		if (region == null) return;

		GeographyRegionMap mapToSave = new GeographyRegionMap();
		mapToSave.setGeography(geography);
		mapToSave.setRegion(region);
		geographyRegionMapRepository.saveAndFlush(mapToSave);
	}

	@Around(value = "removeGeographies() && args(geographyInput)")
	public Object aroundGeographiesDelete(final ProceedingJoinPoint joinPoint, Object geographyInput) throws Throwable {
		if (geographyInput instanceof Geography) {
			removeGeographyRegionMappings((Geography) geographyInput, null);
		} else if (geographyInput instanceof Collection) {
			((Collection<Geography>) geographyInput).forEach(geo -> removeGeographyRegionMappings(geo, null));
		}
		return joinPoint.proceed();
	}

	@Around(value = "removeGeographyRegionMappings() && args(geographyMapInput)")
	public Object aroundGeographyRegionMapDelete(final ProceedingJoinPoint joinPoint, Object geographyMapInput) throws Throwable {
		if (geographyMapInput instanceof GeographyRegionMap) {
			removeGeographyRegionMappings(((GeographyRegionMap) geographyMapInput).getGeography(), List.of(((GeographyRegionMap) geographyMapInput).getId()));
		} else if (geographyMapInput instanceof Collection) {
			var sourceIds = ((Collection<GeographyRegionMap>) geographyMapInput).stream().map(GeographyRegionMap::getId).collect(Collectors.toList());
			((Collection<GeographyRegionMap>) geographyMapInput).stream()
				.forEach(map -> removeGeographyRegionMappings(map.getGeography(), sourceIds));
		}
		return joinPoint.proceed();
	}

	private void removeGeographyRegionMappings(Geography geography, List<Long> sourceIds) {
		QGeography qGeography = QGeography.geography;
		BooleanExpression predicate = qGeography.countryCode.eq(geography.getCountryCode());
		if (geography.getAdm1() != null) predicate = predicate.and(qGeography.adm1.eq(geography.getAdm1()));
		if (geography.getAdm2() != null) predicate = predicate.and(qGeography.adm2.eq(geography.getAdm2()));
		if (geography.getAdm3() != null) predicate = predicate.and(qGeography.adm3.eq(geography.getAdm3()));
		if (geography.getAdm4() != null) predicate = predicate.and(qGeography.adm4.eq(geography.getAdm4()));
		var geographiesToRemoveMappingsQuery = jpaQueryFactory.select(qGeography).from(qGeography).where(predicate);

		var whereExpr = QGeographyRegionMap.geographyRegionMap.geography().in(geographiesToRemoveMappingsQuery);
		if (!CollectionUtils.isEmpty(sourceIds)) {
			whereExpr = whereExpr.and(QGeographyRegionMap.geographyRegionMap.id.notIn(sourceIds));
		}

		jpaQueryFactory.delete(QGeographyRegionMap.geographyRegionMap)
			.where(whereExpr)
			.execute();
	}
}