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.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
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.api.exception.InvalidApiUsageException;
import org.gringlobal.model.Accession;
import org.gringlobal.model.AccessionAction;
import org.gringlobal.model.AccessionInvAnnotation;
import org.gringlobal.model.AccessionInvName;
import org.gringlobal.model.AccessionSource;
import org.gringlobal.model.Inventory;
import org.gringlobal.model.NameGroup;
import org.gringlobal.model.QAccessionInvName;
import org.gringlobal.model.QAccessionSource;
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.AccessionSourceRepository;
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 AccessionSourceRepository accessionSourceRepository;
@Autowired
private AccessionActionService actionService;
@Autowired
private AccessionInvAnnotationService accessionInvAnnotationService;
@Autowired
private ClassPKService classPKService;
private ClassPK accessionClassPk;
@Autowired
private JPAQueryFactory jpaQueryFactory;
@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);
}
@Around(value = "(execution(* org.gringlobal.persistence.AccessionSourceRepository.save(..)) || execution(* org.gringlobal.persistence.AccessionSourceRepository.saveAndFlush(..)) " +
"|| execution(* org.gringlobal.persistence.AccessionSourceRepository.saveAll(..)) || execution(* org.gringlobal.persistence.AccessionSourceRepository.saveAllAndFlush(..)) " +
"|| execution(* org.gringlobal.persistence.AccessionSourceRepository.delete(..)) || execution(* org.gringlobal.persistence.AccessionSourceRepository.deleteAll(..))) && args(sources)")
public Object aroundAccessionSourceManage(final ProceedingJoinPoint joinPoint, Object sources) throws Throwable {
Object result = joinPoint.proceed();
// Check whether the updated sources remain compatible with the existing accession names.
if (sources instanceof Collection) {
List<AccessionSource> sourcesList = new ArrayList<>((Collection<AccessionSource>) sources);
if (!sourcesList.isEmpty()) {
var accessions = sourcesList.stream()
.map(source -> accessionRepository.getReferenceById(source.getAccession().getId()))
.collect(Collectors.toSet());
accessions.forEach(accession -> {
var resultSources = (List<AccessionSource>)accessionSourceRepository.findAll(QAccessionSource.accessionSource.accession().id.eq(accession.getId()));
validateAccessionNames(resultSources, null, accession);
});
}
} else {
AccessionSource source = (AccessionSource) sources;
var accession = accessionRepository.getReferenceById(source.getAccession().getId());
var resultSources = (List<AccessionSource>)accessionSourceRepository.findAll(QAccessionSource.accessionSource.accession().id.eq(accession.getId()));
validateAccessionNames(resultSources, null, accession);
}
return result;
}
@Around(value = "(execution(* org.gringlobal.persistence.AccessionInvNameRepository.save(..)) || execution(* org.gringlobal.persistence.AccessionInvNameRepository.saveAndFlush(..)) " +
"|| execution(* org.gringlobal.persistence.AccessionInvNameRepository.saveAll(..)) || execution(* org.gringlobal.persistence.AccessionInvNameRepository.saveAllAndFlush(..))) && args(accessionInvNames)")
private Object aroundAccessionInvNameSave(final ProceedingJoinPoint joinPoint, Object accessionInvNames) throws Throwable {
if (accessionInvNames instanceof Collection) {
List<AccessionInvName> names = new ArrayList<>((Collection<AccessionInvName>) accessionInvNames);
if (!names.isEmpty()) {
Map<Accession, List<AccessionInvName>> namesGroupedByAccession = names.stream()
.collect(Collectors.groupingBy(name ->
jpaQueryFactory.select(QInventory.inventory.accession()).from(QInventory.inventory)
.where(QInventory.inventory.id.eq(name.getInventory().getId())).fetchOne()
));
namesGroupedByAccession.forEach((accession, accessionNames) -> {
validateAccessionNames(accession.getAccessionSources(), accessionNames, accession);
});
}
} else {
AccessionInvName accessionInvName = (AccessionInvName) accessionInvNames;
var accession = jpaQueryFactory.select(QInventory.inventory.accession()).from(QInventory.inventory)
.where(QInventory.inventory.id.eq(accessionInvName.getInventory().getId())).fetchOne();
validateAccessionNames(accession.getAccessionSources(), List.of(accessionInvName), accession);
}
return joinPoint.proceed();
}
private void validateAccessionNames(List<AccessionSource> sources, List<AccessionInvName> namesToAdd, Accession accession) {
namesToAdd = namesToAdd == null ? new LinkedList<>() : namesToAdd;
List<AccessionInvName> progdoiNames = namesToAdd.stream()
.filter(n -> CommunityCodeValues.ACCESSION_NAME_TYPE_PROGDOI.value.equals(n.getCategoryCode()))
.collect(Collectors.toList());
var accessionNames = (List<AccessionInvName>)accessionInvNameRepository.findAll(
QAccessionInvName.accessionInvName.inventory().accession().id.eq(accession.getId())
.and(QAccessionInvName.accessionInvName.categoryCode.eq(CommunityCodeValues.ACCESSION_NAME_TYPE_PROGDOI.value))
);
if (!CollectionUtils.isEmpty(accessionNames)) {
progdoiNames.addAll(accessionNames.stream().filter(n -> !progdoiNames.contains(n)).collect(Collectors.toSet()));
}
if (CollectionUtils.isEmpty(progdoiNames)) {
return;
}
if (CollectionUtils.isEmpty(sources)) {
throw new InvalidApiUsageException("Accession sources are missing for PROGDOI accession names.");
} else if (progdoiNames.size() > 1 &&
(sources.size() > 1 || !CommunityCodeValues.ACCESSION_SOURCE_TYPE_DEVELOPED.value.equals(sources.get(0).getSourceTypeCode()))
) {
// If there exists only one accession source, and it has type == DEVELOPED, then multiple names of this type are allowed.
// Otherwise, only one PROGDOI name is allowed.
throw new InvalidApiUsageException("Only one DELEVLOPED accession source allows to have multiple PROGDOI accession names");
}
}
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.setSite(accession.getSite());
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);
}
}