AccessionTriggers.java

/*
 * Copyright 2021 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 java.time.Instant;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.IterableUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.genesys.blocks.auditlog.model.AuditAction;
import org.genesys.blocks.auditlog.model.AuditLog;
import org.genesys.blocks.auditlog.service.ClassPKService;
import org.genesys.blocks.model.ClassPK;
import org.gringlobal.model.Accession;
import org.gringlobal.model.AccessionAction;
import org.gringlobal.model.AccessionInvAnnotation;
import org.gringlobal.model.AccessionInvName;
import org.gringlobal.model.Inventory;
import org.gringlobal.model.NameGroup;
import org.gringlobal.model.QAccessionInvName;
import org.gringlobal.model.QInventory;
import org.gringlobal.model.TaxonomySpecies;
import org.gringlobal.model.community.CommunityCodeValues;
import org.gringlobal.persistence.AccessionInvNameRepository;
import org.gringlobal.persistence.AccessionRepository;
import org.gringlobal.persistence.InventoryRepository;
import org.gringlobal.persistence.NameGroupRepository;
import org.gringlobal.service.AccessionActionService;
import org.gringlobal.service.AccessionInvAnnotationService;
import org.gringlobal.service.AccessionInvNameService;
import org.gringlobal.service.AccessionService;
import org.gringlobal.service.InventoryService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Aspect
@Component("accessionTriggers")
@Slf4j
public class AccessionTriggers implements InitializingBean {

	@Autowired
	private AccessionRepository accessionRepository;

	@Autowired
	private InventoryRepository inventoryRepository;

	@Autowired
	public InventoryService inventoryService;

	@Autowired
	private AccessionService accessionService;

	@Autowired
	private NameGroupRepository nameGroupRepository;

	@Autowired
	private AccessionInvNameService accessionInvNameService;

	@Autowired
	private AccessionInvNameRepository accessionInvNameRepository;

	@Autowired
	private AccessionActionService actionService;

	@Autowired
	private AccessionInvAnnotationService accessionInvAnnotationService;

	@Autowired
	private ClassPKService classPKService;

	private ClassPK accessionClassPk;

	@Override
	public void afterPropertiesSet() throws Exception {
		accessionClassPk = classPKService.getClassPk(Accession.class);
	}

	@AfterReturning(value = "(execution(* org.genesys.blocks.auditlog.persistence.AuditLogRepository.save(..)) || execution(* org.genesys.blocks.auditlog.persistence.AuditLogRepository.saveAndFlush(..))) && args(auditLog)")
	public void afterSaveAuditLogs(JoinPoint joinPoint, AuditLog auditLog) throws Throwable {
		log.trace("Checking 1 saved auditLog, cpk={}", this.accessionClassPk);
		afterSaveAuditLogs(null, List.of(auditLog));
	}

	@AfterReturning(value = "execution(* org.genesys.blocks.auditlog.service.*.addAuditLogs(..))", returning = "auditLogs")
	public void afterSaveAuditLogs(JoinPoint joinPoint, Collection<AuditLog> auditLogs) throws Throwable {
		log.trace("Checking {} saved auditLogs, cpk={}", auditLogs.size(), this.accessionClassPk);
		var changedAccessions = new HashMap<Long, HashMap<String, Pair<Object, Object>>>();
		auditLogs.stream().filter(this::isAccessionChangeLog).forEach(auditLog -> {
			var changes = changedAccessions.get(auditLog.getEntityId());
			if (changes == null) {
				changedAccessions.put(auditLog.getEntityId(), changes = new HashMap<String, Pair<Object, Object>>());
			}
			log.trace("{}#{} {} -> {}", auditLog.getEntityId(), auditLog.getPropertyName(), auditLog.getPreviousState(), auditLog.getNewState());
			log.trace("{}#{} {} -> {}", auditLog.getEntityId(), auditLog.getPropertyName(), auditLog.getPreviousEntity(), auditLog.getNewEntity());
			changes.put(auditLog.getPropertyName(), Pair.of(auditLog.getPreviousEntity(), auditLog.getNewEntity()));
		});
		changedAccessions.forEach((accessionId, changes) -> {
			perhapsRecordChanges(accessionId, changes);
		});
	}

	// Utility
	private boolean isAccessionChangeLog(final AuditLog auditLog) {
		return auditLog != null && auditLog.getAction() == AuditAction.UPDATE && auditLog.getClassPk().equals(this.accessionClassPk);
	}

	private void perhapsRecordChanges(Long accessionId, HashMap<String, Pair<Object, Object>> changes) {
		var taxonmySpeciesChange = changes.get("taxonomySpecies");
		if (taxonmySpeciesChange != null) {
			recordAccessionSpeciesChangesIfNeeded(accessionId, (TaxonomySpecies) taxonmySpeciesChange.getKey(), (TaxonomySpecies) taxonmySpeciesChange.getValue());
		}

		var accessionNumberChange = changes.get("accessionNumber");
		if (accessionNumberChange != null) {
			// We need accessionNumberPart1
			var accessionNumberPart1Change = changes.get("accessionNumberPart1");
			if (accessionNumberPart1Change != null) {
				recordAccessionNumberIfNeeded(accessionId, (String) accessionNumberChange.getKey(), (String) accessionNumberChange.getValue(), (String) accessionNumberPart1Change.getKey(), (String) accessionNumberPart1Change.getValue());
			} else {
				recordAccessionNumberIfNeeded(accessionId, (String) accessionNumberChange.getKey(), (String) accessionNumberChange.getValue(), null, null);
			}
		}
	}

	@Around(value = "(execution(* org.gringlobal.persistence.AccessionRepository.save(..)) || execution(* org.gringlobal.persistence.AccessionRepository.saveAndFlush(..))) && args(accession)")
	public Object aroundAccessionSave(final ProceedingJoinPoint joinPoint, final Accession accession) throws Throwable {
		// assign the accessionNumberPart2 if needed
		assignAccessionNumberPart2IfNeeded(accession);

		if (!accession.isNew()) {
			// it's updating, no need to assure systemInventory
			Accession updatedAccession = (Accession) joinPoint.proceed();
			// rename system inventories after updating
			renameInventories(updatedAccession);
			return updatedAccession;
		} else {
			// inserting new accession entry
			Accession createdAccession = (Accession) joinPoint.proceed();

			log.info("Assure system inventory for {}", createdAccession);
			inventoryService.assureSystemInventory(createdAccession);
	
			recordAccessionNumber(createdAccession.getId(), createdAccession.getAccessionNumber(), createdAccession.getAccessionNumberPart1());
			recordReceivedAccessionSpecies(createdAccession);
			return createdAccession;
		}
	}

	@Before(value = "execution(* org.gringlobal.persistence.AccessionRepository.delete(*)) && args(accession)")
	public void beforeDeleteAccession(final JoinPoint joinPoint, final Accession accession) throws Throwable {
		accessionService.deleteDefaultInventory(accession);
	}

	@Before(value = "execution(* org.gringlobal.persistence.AccessionRepository.deleteAll(Iterable)) && args(accessions)")
	public void beforeDeleteAccessions(final JoinPoint joinPoint, final Collection<Accession> accessions) throws Throwable {
		log.debug("Many accessions are being deleted! {}", accessions);
		accessions.forEach(accessionService::deleteDefaultInventory);
	}

	private void assignAccessionNumberPart2IfNeeded(Accession accession) {
		if (AccessionService.AUTO_GENERATE_VALUE.equals(accession.getAccessionNumberPart2())) {
			// assign the next within accessionNumberPart1
			long maxPart2 = accessionRepository.findMaxNumberPart2(accession.getAccessionNumberPart1());
			log.debug("Using next numberPart2 {}+1 for accession {}", maxPart2, accession.getAccessionNumberPart1());
			accession.setAccessionNumberPart2(1 + maxPart2);
		}
	}

	/**
	 * Rename system inventories to match accession number parts
	 *
	 * @param accession the accession
	 */
	private void renameInventories(final Accession accession) {
		if (accession.getInventories() != null) {
			log.info("Renaming accession SYSTEM inventories for updated accession {}", accession.getId());
			inventoryRepository.findAll(QInventory.inventory.accession().eq(accession).and(QInventory.inventory.formTypeCode.eq(Inventory.SYSTEM_INVENTORY_FTC)))
				// rename
				.forEach(inventory -> {
					inventory.setInventoryNumberPart1(accession.getAccessionNumberPart1());
					inventory.setInventoryNumberPart2(accession.getAccessionNumberPart2());
					inventory.setInventoryNumberPart3(accession.getAccessionNumberPart3());
					inventoryRepository.save(inventory);
				});
		}
	}

	private void recordAccessionNumberIfNeeded(long accessionId, String oldNumber, String newNumber, String oldNumberPart1, String newNumberPart1) {
		log.trace("Recording name change aid={} {}/{} -> {}/{}", accessionId, oldNumberPart1, oldNumber, newNumberPart1, newNumber);
		if (newNumber != null && !newNumber.equals(oldNumber)) {
			log.debug("Recording name change aid={} {}/{} -> {}/{}", accessionId, oldNumberPart1, oldNumber, newNumberPart1, newNumber);
			var accession = new Accession(accessionId);

			var qAIN = QAccessionInvName.accessionInvName;
			if (IterableUtils.isEmpty(accessionInvNameRepository.findAll(
				// system inventory of accession
				qAIN.inventory().formTypeCode.eq(Inventory.SYSTEM_INVENTORY_FTC).and(qAIN.inventory().accession().eq(accession))
					// name type
					.and(qAIN.categoryCode.eq(CommunityCodeValues.ACCESSION_NAME_TYPE_SITE.value))))) {
				// previous accession number isn't yet registered, do it now
				recordAccessionNumber(accessionId, oldNumber, oldNumberPart1);
			}
			// register changed accession number
			recordAccessionNumber(accessionId, newNumber, newNumberPart1);

			// register new accession action
			var action = new AccessionAction();
			action.setAccession(accession);
			action.setIsWebVisible("N");
			action.setActionNameCode(CommunityCodeValues.ACCESSION_ACTION_ACCNUMBERD.value);
			action.setNote("Changed " + oldNumber + " to " + newNumber);
			action.setStartedDate(Instant.now());
			action.setCompletedDate(action.getStartedDate());
			action.setStartedDateCode(CommunityCodeValues.DATE_FORMAT_DATETIME.value);
			action.setCompletedDateCode(CommunityCodeValues.DATE_FORMAT_DATETIME.value);
			action = actionService.create(action);
			assert (action != null);
			log.debug("Added action {}: {}", action.getActionNameCode(), action.getNote());
		} else {
			log.trace("Not recording name change {} {}", oldNumber, newNumber);
		}
	}

	private void recordAccessionNumber(long accessionId, String accessionNumber, String accessionNumberPart1) {
		log.debug("Registering accession number: {}", accessionNumber);
		var systemInventory = inventoryRepository.getSystemInventory(new Accession(accessionId));
		NameGroup nameGroup = null;
		if (StringUtils.isNotBlank(accessionNumberPart1)) {
			// link it with the name group
			nameGroup = nameGroupRepository.findByGroupName(accessionNumberPart1);
			log.trace("Name group {} {}", accessionNumberPart1, nameGroup);
		}

		// Check if the name exists
		var qAIN = QAccessionInvName.accessionInvName;
		var predicate = 
			// inventory
			qAIN.inventory().eq(systemInventory)
			// plant name
			.and(qAIN.plantName.eq(accessionNumber))
			// category code
			.and(qAIN.categoryCode.eq(CommunityCodeValues.ACCESSION_NAME_TYPE_SITE.value))
			// name group
			.and(nameGroup == null ? qAIN.nameGroup().isNull() : qAIN.nameGroup().eq(nameGroup));

		if (! accessionInvNameRepository.exists(predicate)) {
			AccessionInvName invName = new AccessionInvName();
			invName.setPlantName(accessionNumber);
			invName.setPlantNameRank(1080);
			invName.setCategoryCode(CommunityCodeValues.ACCESSION_NAME_TYPE_SITE.value);
			invName.setInventory(systemInventory);
			invName.setNameGroup(nameGroup);
			var ain = accessionInvNameService.create(invName);
			assert(ain != null);
		} else {
			log.trace("Name already exists");
		}
	}

	private void recordAccessionSpeciesChangesIfNeeded(long accessionId, TaxonomySpecies oldTs, TaxonomySpecies newTs) {
		log.debug("recordAccessionSpeciesChangesIfNeeded: orig={} curr={}", oldTs, newTs);
		if (oldTs != null && !Objects.equals(oldTs.getId(), newTs.getId())) {
			AccessionInvAnnotation annotation = new AccessionInvAnnotation();
			annotation.setAnnotationTypeCode(CommunityCodeValues.ANNOTATION_TYPE_RE_IDENT.value);
			annotation.setInventory(inventoryRepository.getSystemInventory(new Accession(accessionId)));
			annotation.setOldTaxonomySpecies(oldTs);
			annotation.setNewTaxonomySpecies(newTs);
			annotation.setAnnotationDate(new Date());
			annotation.setAnnotationDateCode(CommunityCodeValues.DATE_FORMAT_DATETIME.value);
			var aia = accessionInvAnnotationService.create(annotation);
			assert(aia != null);
		} else {
			log.trace("No change detected old={} new={}", oldTs, newTs);
		}
	}

	private void recordReceivedAccessionSpecies(Accession accession) {
		assert(accession.getId() != null);
		AccessionInvAnnotation annotation = new AccessionInvAnnotation();
		annotation.setAnnotationTypeCode(CommunityCodeValues.ANNOTATION_TYPE_RECEIVED.value);
		annotation.setInventory(inventoryRepository.getSystemInventory(accession));
		annotation.setAnnotationDate(new Date());
		annotation.setAnnotationDateCode(CommunityCodeValues.DATE_FORMAT_DATETIME.value);
		annotation.setNewTaxonomySpecies(accession.getTaxonomySpecies());
		var aia = accessionInvAnnotationService.create(annotation);
		assert(aia != null);
	}

}