AccessionMCPDConverter.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.worker;
import static org.gringlobal.model.community.CommunityAppSettings.MCPD_ACCEURL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.gringlobal.component.elastic.AppContextHelper;
import org.gringlobal.model.Accession;
import org.gringlobal.model.AccessionInvName;
import org.gringlobal.model.AccessionSource;
import org.gringlobal.model.Inventory;
import org.gringlobal.model.TaxonomySpecies;
import org.gringlobal.model.community.AccessionMCPD;
import org.gringlobal.model.community.CommunityCodeValues;
import org.gringlobal.model.community.IWebVisible;
import org.gringlobal.service.AppSettingsService;
import org.gringlobal.service.CodeValueService;
import org.gringlobal.service.TemplatingService;
import org.gringlobal.util.MCPDDate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Component;
import com.github.mustachejava.Mustache;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.extern.slf4j.Slf4j;
/**
* The Class AccessionMCPDConverter.
*/
@Component
@Slf4j
public class AccessionMCPDConverter {
@Autowired
private CodeValueService codeValueService;
@Autowired
private AppSettingsService appSettingsService;
@Autowired
private TemplatingService templatingService;
private final Cache<Pair<String, String>, Optional<String>> mcpdCodeValueCache = CacheBuilder.newBuilder().maximumSize(200).expireAfterAccess(30, TimeUnit.SECONDS).build();
private final Cache<String, Optional<Mustache>> acceUrlCompiledTemplateCache = CacheBuilder.newBuilder().maximumSize(1).expireAfterAccess(30, TimeUnit.SECONDS).build();
private final Cache<TaxonomySpecies, Optional<String>> taxonomyCropNameCache = CacheBuilder.newBuilder().maximumSize(200).expireAfterAccess(30, TimeUnit.SECONDS).build();
/** Map MCPD field to a type of ACCESSION_NAME_TYPE */
private final Cache<String, Optional<String>> mcpdAccessionNameType = CacheBuilder.newBuilder().maximumSize(10).expireAfterAccess(30, TimeUnit.SECONDS).build();
public void dropAcceUrlTemplateCache() {
acceUrlCompiledTemplateCache.invalidateAll();
}
/**
* Figure out which ACCESSION_NAME_TYPE has MCPD translation that matches {@code mcpdField}.
*
* @param mcpdField COLLMISSID, ACCENAME, etc.
* @return The corresponding code value
*/
private Optional<String> findAccessionNameType(String mcpdField) {
if (mcpdField == null) {
return Optional.empty();
}
try {
return mcpdAccessionNameType.get(mcpdField, () -> {
return Optional.ofNullable(codeValueService.findCodeValueOfMCPD(CommunityCodeValues.ACCESSION_NAME_TYPE, mcpdField));
});
} catch (ExecutionException e) {
throw new RuntimeException("Error reading code value for MCPD", e);
}
}
private Optional<String> findMcpdOfCodeValue(String groupName, String value) {
if (value == null) {
return Optional.empty();
}
try {
return mcpdCodeValueCache.get(Pair.of(groupName, value), () -> {
return Optional.ofNullable(codeValueService.findMcpdOfCodeValue(groupName, value));
});
} catch (ExecutionException e) {
throw new RuntimeException("Error reading MCPD code value", e);
}
}
private Optional<Mustache> findAcceUrlTemplate() {
try {
return acceUrlCompiledTemplateCache.get(MCPD_ACCEURL.name, () -> {
Optional<String> setting = appSettingsService.getSetting(MCPD_ACCEURL.categoryTag, MCPD_ACCEURL.name, String.class);
return setting.map(template -> {
// If blank or set to default value then ignore
if (StringUtils.isBlank(template) || Objects.equals(template, MCPD_ACCEURL.value)) return null;
return templatingService.compileTemplate(template);
});
});
} catch (ExecutionException e) {
throw new RuntimeException("Error reading ACCEURL app setting", e);
}
}
private Optional<String> findCropName(TaxonomySpecies taxonomySpecies) {
try {
return taxonomyCropNameCache.get(taxonomySpecies, () -> {
log.debug("Looking for crop name of taxonomySpecies id={} {}", taxonomySpecies.getId(), taxonomySpecies.getName());
var cropMap = taxonomySpecies.getTaxonomyCrops();
if (cropMap.size() > 0) {
var firstTaxonomyCrop = cropMap.get(0);
// Use the genebank's name of the crop
if (firstTaxonomyCrop.getCrop() != null) {
return Optional.ofNullable(firstTaxonomyCrop.getCrop().getName());
}
// Use common crop name if available
if (StringUtils.isNotBlank(firstTaxonomyCrop.getCommonCropName())) {
return Optional.ofNullable(firstTaxonomyCrop.getCommonCropName());
}
}
// Check current taxonomy (if not the same)
if (taxonomySpecies.getCurrentTaxonomySpecies() != null
&& !taxonomySpecies.getCurrentTaxonomySpecies().getId().equals(taxonomySpecies.getId())) {
log.trace("Looking for crop name of current species of {} currentTaxonomySpecies.id={}", taxonomySpecies.getName(), taxonomySpecies.getCurrentTaxonomySpecies().getId());
return findCropName(taxonomySpecies.getCurrentTaxonomySpecies());
}
// No CROPNAME found.
return Optional.empty();
});
} catch (ExecutionException e) {
throw new RuntimeException("Error fetching CROPNAME for " + taxonomySpecies.getName(), e);
}
}
public AccessionMCPD convert(Accession accession) {
assert(accession != null);
assert(accession.isNew() == false);
AccessionMCPD mcpd = new AccessionMCPD();
mcpd.id = accession.getId();
mcpd.isWebVisible = accession.getIsWebVisible();
mcpd.puid = accession.getDoi();
mcpd.instCode = accession.getSite().getFaoInstituteNumber();
mcpd.acceNumb = accession.getAccessionNumber();
mcpd.acceName = accession.getPreferredName();
mcpd.acqDate = MCPDDate.convert(accession.getInitialReceivedDate(), accession.getInitialReceivedDateCode());
try {
mcpd.sampStat = Integer.parseInt(findMcpdOfCodeValue(CommunityCodeValues.IMPROVEMENT_LEVEL, accession.getImprovementStatusCode()).orElse(null));
} catch (NumberFormatException e) {}
try {
mcpd.mlsStat = Integer.parseInt(findMcpdOfCodeValue(CommunityCodeValues.ACCESSION_MLS_STATUS, accession.getMlsStatus()).orElse(null));
} catch (NumberFormatException e) {}
// mcpd.remarks = accession.getNote();
mcpd.curationType = findMcpdOfCodeValue(CommunityCodeValues.ACCESSION_CURATION_TYPE, accession.getCurationTypeCode()).orElse(null);
TaxonomySpecies taxonomySpecies = accession.getTaxonomySpecies();
mcpd.genus = taxonomySpecies.getTaxonomyGenus().getName();
mcpd.species = taxonomySpecies.getSpecificEpithet();
mcpd.spAuthor = taxonomySpecies.getSpeciesAuthority();
mcpd.subtaxa = taxonomySpecies.getSubTaxon();
if (mcpd.subtaxa != null) {
mcpd.subtAuthor = taxonomySpecies.getNameAuthority();
}
mcpd.cropName = findCropName(taxonomySpecies).orElse(null);
var webVisibleSources = accession.getAccessionSources().stream().filter(IWebVisible::isWebVisible).collect(Collectors.toList());
// MCPD ORIGCTY: Geography that is flagged as `isSource` provides the country of provenance
var originSource = webVisibleSources.stream().filter(AccessionSource::isOrigin).findFirst().orElse(null);
if (originSource != null) {
// source with "isOrigin=Y" is the ORIGCTY
if (originSource.getGeography() != null) {
mcpd.origCty = StringUtils.defaultIfBlank(
// Use MCPD translation
findMcpdOfCodeValue(CommunityCodeValues.GEOGRAPHY_COUNTRY_CODE, originSource.getGeography().getCountryCode()).orElse(null),
// use country code directly if not translated
originSource.getGeography().getCountryCode());
}
}
// COLLECTED
var collectedSource = webVisibleSources.stream().filter((source) -> CommunityCodeValues.ACCESSION_SOURCE_TYPE_COLLECTED.value.equals(source.getSourceTypeCode())).findFirst().orElse(null);
if (collectedSource != null) {
mcpd.collSite = collectedSource.getCollectorVerbatimLocality();
mcpd.collDate = MCPDDate.convert(collectedSource.getSourceDate(), collectedSource.getSourceDateCode());
try {
mcpd.collSrc = Integer.parseInt(findMcpdOfCodeValue(CommunityCodeValues.ACCESSION_SOURCE_HABITAT_TYPE, collectedSource.getAcquisitionSource()).orElse(null));
} catch (NumberFormatException e) {}
mcpd.decLatitude = collectedSource.getLatitude();
mcpd.decLongitude = collectedSource.getLongitude();
mcpd.coordDatum = collectedSource.getGeoreferenceDatum();
mcpd.coordUncert = collectedSource.getUncertainty();
mcpd.geoRefMeth = collectedSource.getGeoreferenceProtocolCode();
mcpd.elevation = collectedSource.getElevationMeters();
// first Cooperator
var firstCooperator = collectedSource.getCooperators().stream().findFirst().orElse(null);
if (firstCooperator != null) {
mcpd.collCode = firstCooperator.getFaoInstituteNumber();
mcpd.collName = firstCooperator.getOrganization();
// Full address
mcpd.collInstAddress = Stream.of(firstCooperator.getAddressLine1(), firstCooperator.getAddressLine2(), firstCooperator.getAddressLine3(), firstCooperator.getCity())
.filter(StringUtils::isNotBlank).collect(Collectors.joining(", "));
}
}
// DONATED
var donorSource = webVisibleSources.stream().filter((source) -> CommunityCodeValues.ACCESSION_SOURCE_TYPE_DONATED.value.equals(source.getSourceTypeCode())).findFirst().orElse(null);
if (donorSource != null) {
if (donorSource.getCooperators().size() > 0) {
var cooperator = donorSource.getCooperators().get(0);
mcpd.donorCode = cooperator.getFaoInstituteNumber();
mcpd.donorName = cooperator.getOrganization();
}
}
// DEVELOPED
var developSource = webVisibleSources.stream().filter((source) -> CommunityCodeValues.ACCESSION_SOURCE_TYPE_DEVELOPED.value.equals(source.getSourceTypeCode())).findFirst().orElse(null);
if (developSource != null) {
if (developSource.getCooperators().size() > 0) {
var cooperator = developSource.getCooperators().get(0);
mcpd.bredCode = cooperator.getFaoInstituteNumber();
mcpd.bredName = cooperator.getOrganization();
}
}
List<String> duplSite = new ArrayList<>();
List<String> duplInstName = new ArrayList<>();
Stream.of(accession.getBackupLocation1Site(), accession.getBackupLocation2Site()).filter(AccessionMCPDConverter::notNull).forEach((backupSite) -> {
duplSite.add(backupSite.getFaoInstituteNumber());
duplInstName.add(backupSite.getSiteLongName());
});
mcpd.duplSite = StringUtils.trimToNull(duplSite.stream().filter(AccessionMCPDConverter::notNull).collect(Collectors.joining(";")));
mcpd.duplInstName = StringUtils.trimToNull(duplInstName.stream().filter(AccessionMCPDConverter::notNull).collect(Collectors.joining(";")));
if (accession.getExploration() != null) {
mcpd.collMissid = accession.getExploration().getExplorationNumber();
}
var accessionNames = accession.getNames();
if (accessionNames == null) {
accessionNames = AppContextHelper.loadNames(accession, Inventory.SYSTEM_INVENTORY_FTC); // only system inventory
}
var webVisibleNames = accessionNames.stream()
// only public records
.filter(IWebVisible::isWebVisible)
// order by name rank
.sorted(AccessionInvName::orderByPlantNameRank)
// as list
.collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(webVisibleNames)) {
var nameTypeACCENAME = findAccessionNameType("ACCENAME");
if (nameTypeACCENAME.isPresent()) {
mcpd.acceName = webVisibleNames.stream().filter((name) -> Objects.equals(nameTypeACCENAME.get(), name.getCategoryCode()))
// plant name
.map(AccessionInvName::getPlantName)
// first only
.findFirst().orElse(null);
} else {
mcpd.acceName = webVisibleNames.get(0).getPlantName(); // top name is accession name
}
var nameTypeDONORNUMB = findAccessionNameType("DONORNUMB").orElse(CommunityCodeValues.ACCESSION_NAME_TYPE_DONOR.value);
mcpd.donorNumb = webVisibleNames.stream().filter((name) -> Objects.equals(nameTypeDONORNUMB, name.getCategoryCode()))
// plant name
.map(AccessionInvName::getPlantName)
// first only
.findFirst().orElse(null);
var nameTypeCOLLNUMB = findAccessionNameType("COLLNUMB").orElse(CommunityCodeValues.ACCESSION_NAME_TYPE_COLLECTOR.value);
mcpd.collNumb = webVisibleNames.stream().filter((name) -> Objects.equals(nameTypeCOLLNUMB, name.getCategoryCode()))
// plant name
.map(AccessionInvName::getPlantName)
// first only
.findFirst().orElse(null);
if (mcpd.collMissid == null) {
var nameTypeCOLLMISSID = findAccessionNameType("COLLMISSID").orElse(CommunityCodeValues.ACCESSION_NAME_TYPE_EXPLORATION.value);
mcpd.collMissid = webVisibleNames.stream().filter(name -> Objects.equals(nameTypeCOLLMISSID, name.getCategoryCode()))
// plant name
.map(AccessionInvName::getPlantName)
// first only
.findFirst().orElse(null);
}
var otherNumb = webVisibleNames.stream().map(AccessionInvName::getPlantName).distinct().collect(Collectors.toList()); // Using list here so the order of names is preserved
/*@formatter:off*/
otherNumb.remove(mcpd.acceNumb);
if (mcpd.puid != null) otherNumb.remove(mcpd.puid);
if (mcpd.acceName != null) otherNumb.remove(mcpd.acceName);
if (mcpd.donorCode != null) otherNumb.remove(mcpd.donorCode);
if (mcpd.donorNumb != null) otherNumb.remove(mcpd.donorNumb);
if (mcpd.collCode != null) otherNumb.remove(mcpd.collCode);
if (mcpd.collNumb != null) otherNumb.remove(mcpd.collNumb);
if (mcpd.collMissid != null) otherNumb.remove(mcpd.collMissid);
/*@formatter:on*/
mcpd.otherNumb = StringUtils.trimToNull(otherNumb.stream().collect(Collectors.joining(";")));
}
if (accession.getAccessionPedigree() != null) {
mcpd.ancest = accession.getAccessionPedigree().getDescription();
}
findAcceUrlTemplate().ifPresent(compiled -> {
mcpd.acceUrl = StringUtils.stripToNull(templatingService.fillTemplate(compiled, Map.of("accession", accession)));
});
if (accession.getCurationTypeCode() != null) { // Curation Type overrides historical
mcpd.historical = Objects.equals(CommunityCodeValues.ACCESSION_CURATION_TYPE_HISTORICAL.value, accession.getCurationTypeCode());
} else { // Or curation type is not specified
try {
mcpd.historical = not(toBoolean(findMcpdOfCodeValue(CommunityCodeValues.ACCESSION_STATUS, accession.getStatusCode()).orElse(null)));
} catch (NumberFormatException e) {}
}
// Availability and storage
if (mcpd.historical != null && mcpd.historical == true) {
// If historical, then there is no availability and no storage
mcpd.availability = false;
mcpd.storage = "";
} else {
// Otherwise scan inventories for details
Boolean availability = null;
Set<String> storage = new HashSet<>();
for (var inv : accession.getInventories()) {
if (inv.isSystemInventory()) { // skip system inventory
continue;
}
Double quantityOnHand = inv.getQuantityOnHand();
if (quantityOnHand == null) { // consider only inventories with specified quantity
continue;
}
if (availability == null || availability == false) {
if (Objects.equals("Y", inv.getIsDistributable()) // Is distributale
&& Objects.equals("Y", inv.getIsAvailable()) // TODO We should not be considering #isAvailable
&& quantityOnHand > 0 // Has some quantity
&& (inv.getDistributionCriticalQuantity() == null || quantityOnHand > inv.getDistributionCriticalQuantity()) // Sufficient quantity on hand
) {
availability = true;
} else {
availability = false;
}
}
if (quantityOnHand <= 0) { // Do not consider empty inventories for storage
continue;
}
var storageTypeCode = inv.getInventoryMaintenancePolicy().getStorageTypeCode();
String storageType = findMcpdOfCodeValue(CommunityCodeValues.STORAGE_TYPE, storageTypeCode).orElse(null);
if (storageType != null) {
storage.add(storageType);
}
}
mcpd.availability = availability;
mcpd.storage = storage.stream().sorted().collect(Collectors.joining(";"));
}
mcpd.lastModified = accession.getModifiedDate();
return mcpd;
}
private static Boolean not(Boolean bool) {
return bool == null ? null : !bool;
}
private static Boolean toBoolean(String booleanStringOfSomeKind) {
Boolean result = null;
if (!StringUtils.isBlank(booleanStringOfSomeKind)) {
if ("1".equals(booleanStringOfSomeKind) || "true".equalsIgnoreCase(booleanStringOfSomeKind) || "yes".equalsIgnoreCase(booleanStringOfSomeKind)) {
result = true;
} else if ("0".equals(booleanStringOfSomeKind) || "false".equalsIgnoreCase(booleanStringOfSomeKind) || "no".equalsIgnoreCase(booleanStringOfSomeKind)) {
result = false;
}
}
return result;
}
private static <T> boolean notNull(T obj) {
return obj != null;
}
public static Stream<String> streamSplit(String regexp, String value) {
if (StringUtils.isBlank(value)) {
return Stream.empty();
}
return Stream.of(value.split(regexp)).filter(StringUtils::isNotBlank).map(String::strip);
}
}