BrAPIv2Mapper.java
/*
* Copyright 2023 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.brapi.v2;
import java.text.NumberFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.genesys.blocks.model.filters.StringFilter;
import org.gringlobal.custom.elasticsearch.SearchException;
import org.gringlobal.model.Accession;
import org.gringlobal.model.AccessionSource;
import org.gringlobal.model.CropTrait;
import org.gringlobal.model.CropTraitCode;
import org.gringlobal.model.community.CommunityCodeValues;
import org.gringlobal.model.community.CommunityCodeValues.CodeValueDef;
import org.gringlobal.service.CropTraitCodeTranslationService.TranslatedCropTraitCode;
import org.gringlobal.service.CropTraitTranslationService.TranslatedCropTrait;
import org.gringlobal.service.SiteService;
import org.gringlobal.service.TaxonomySpeciesCRUDService;
import org.gringlobal.service.filter.SiteFilter;
import org.gringlobal.worker.AccessionMCPDConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Component;
import uk.ac.hutton.ics.brapi.resource.germplasm.attribute.Category;
import uk.ac.hutton.ics.brapi.resource.germplasm.attribute.Method;
import uk.ac.hutton.ics.brapi.resource.germplasm.attribute.Scale;
import uk.ac.hutton.ics.brapi.resource.germplasm.attribute.Scale.DataType;
import uk.ac.hutton.ics.brapi.resource.germplasm.attribute.Trait;
import uk.ac.hutton.ics.brapi.resource.germplasm.attribute.ValidValues;
import uk.ac.hutton.ics.brapi.resource.germplasm.germplasm.Collection;
import uk.ac.hutton.ics.brapi.resource.germplasm.germplasm.Collsite;
import uk.ac.hutton.ics.brapi.resource.germplasm.germplasm.Germplasm;
import uk.ac.hutton.ics.brapi.resource.germplasm.germplasm.Institute;
import uk.ac.hutton.ics.brapi.resource.germplasm.germplasm.Mcpd;
import uk.ac.hutton.ics.brapi.resource.germplasm.germplasm.McpdDonor;
import uk.ac.hutton.ics.brapi.resource.phenotyping.observation.ObservationVariable;
@Component
public class BrAPIv2Mapper {
@Autowired
@Lazy
private AccessionMCPDConverter accessionMcpdConverter;
@Autowired
@Lazy
private SiteService siteService;
@Autowired
@Lazy
private TaxonomySpeciesCRUDService taxonomySpeciesService;
public static <A, B> List<B> map(List<A> source, Function<A, B> mapper) {
return source.stream().map(mapper).collect(Collectors.toList());
}
public static <A, B> Page<B> map(Page<A> source, Function<A, B> mapper) {
return source.map(mapper);
}
/**
* Allows mapping of {@code null} objects to {@code null}.
*
* @param <SOURCE> source type
* @param <TARGET> target type
* @param source Source object, may be null
* @param mapper mapping function from {@code SOURCE} to {@code TARGET} type
* @return
*/
public static <SOURCE, TARGET> TARGET mapNullable(SOURCE source, Function<SOURCE, TARGET> mapper) {
if (source == null)
return null;
return mapper.apply(source);
}
private String map(Long value) {
return value == null ? null : Long.toString(value);
}
private static ThreadLocal<NumberFormat> df = new ThreadLocal<>() {
protected NumberFormat initialValue() {
return NumberFormat.getNumberInstance(Locale.ROOT);
};
};
private String map(Double value) {
return value == null ? null : df.get().format(value);
}
public Germplasm map(Accession a) {
var dto = new Germplasm();
dto.setGermplasmDbId(Long.toString(a.getId()));
dto.setAccessionNumber(a.getAccessionNumber());
if (a.getInitialReceivedDate() != null)
dto.setAcquisitionDate(Instant.ofEpochMilli(a.getInitialReceivedDate().getTime()).atOffset(ZoneOffset.UTC)
.format(DateTimeFormatter.ISO_LOCAL_DATE));
dto.setDefaultDisplayName(a.getAccessionNumber());
dto.setGenus(a.getTaxonomySpecies().getTaxonomyGenus().getName());
dto.setSpecies(a.getTaxonomySpecies().getSpecificEpithet());
dto.setSpeciesAuthority(a.getTaxonomySpecies().getSpeciesAuthority());
dto.setGermplasmName(a.getPreferredName());
dto.setGermplasmPUI(a.getDoi());
dto.setSubtaxa(a.getTaxonomySpecies().getSubTaxon());
if (dto.getSubtaxa() != null) dto.setSubtaxaAuthority(a.getTaxonomySpecies().getNameAuthority());
dto.setInstituteCode(a.getSite().getFaoInstituteNumber());
// dto.setSynonyms(a.getNames().stream()
// // filter
// .filter(name -> Objects.equals("Y", name.getIsWebVisible()))
// // map to Synonym
// .map(name -> new Synonym().setSynonym(name.getPlantName()).setType(name.getCategoryCode()))
// // collect
// .collect(Collectors.toList())
// );
return dto;
}
public Accession map(Germplasm a) {
var dto = new Accession();
if (a.getGermplasmDbId() != null) dto.setId(Long.parseLong(a.getGermplasmDbId()));
dto.setAccessionNumber(a.getAccessionNumber());
dto.setAccessionNumberPart1(a.getAccessionNumber()); // Can we split this?
if (a.getAcquisitionDate() != null) {
var receivedDate = LocalDate.parse(a.getAcquisitionDate(), DateTimeFormatter.ISO_LOCAL_DATE);
dto.setInitialReceivedDate(Date.from(receivedDate.atStartOfDay().atOffset(ZoneOffset.UTC).toInstant()));
dto.setInitialReceivedDateCode(CommunityCodeValues.DATE_FORMAT_DATE.value);
}
// dto.setGenus(a.getTaxonomySpecies().getTaxonomyGenus().getName());
// dto.setSpecies(a.getTaxonomySpecies().getSpecificEpithet());
// dto.setSpeciesAuthority(a.getTaxonomySpecies().getSpeciesAuthority());
// dto.setSubtaxa(a.getTaxonomySpecies().getSubTaxon());
// dto.setSubtaxaAuthority(a.getTaxonomySpecies().getSubTaxon())
// dto.setPreferredName(a.getGermplasmName());
dto.setDoi(a.getGermplasmPUI());
// Site
var siteFilter = new SiteFilter()
.faoInstituteNumber(new StringFilter().eq(Set.of(a.getInstituteCode())));
try {
var sites = siteService.list(siteFilter, Pageable.ofSize(1));
if (sites.hasContent()) dto.setSite(sites.getContent().get(0));
} catch (SearchException e) {
}
var taxonomySpecies = taxonomySpeciesService.fromMCPD(a.getGenus(), a.getSpecies(), a.getSpeciesAuthority(), a.getSubtaxa(), a.getSubtaxaAuthority());
dto.setTaxonomySpecies(taxonomySpecies);
return dto;
}
public Mcpd mapMcpd(Accession a) {
var mcpd = accessionMcpdConverter.convert(a);
var dto = new Mcpd();
dto.setGermplasmDbId(Long.toString(a.getId()));
dto.setAccessionNumber(mcpd.acceNumb);
dto.setGermplasmPUI(mcpd.puid);
dto.setInstituteCode(mcpd.instCode);
dto.setAccessionNumber(mcpd.acceNumb);
dto.setAcquisitionDate(mcpd.acqDate);
dto.setGenus(mcpd.genus);
dto.setSpecies(mcpd.species);
dto.setSpeciesAuthority(mcpd.spAuthor);
dto.setSubtaxon(mcpd.subtaxa);
dto.setSubtaxonAuthority(mcpd.subtAuthor);
if (mcpd.collSrc != null) dto.setAcquisitionSourceCode(mcpd.collSrc.toString());
dto.setAncestralData(mcpd.ancest);
if (mcpd.sampStat != null) dto.setBiologicalStatusOfAccessionCode(mcpd.sampStat.toString());
dto.setBreedingInstitutes(toBrapiInstitutes(a.getAccessionSources(), CommunityCodeValues.ACCESSION_SOURCE_TYPE_DEVELOPED));
dto.setCollectingInfo(new Collection()
.setCollectingDate(mcpd.collDate)
.setCollectingInstitutes(toBrapiInstitutes(a.getAccessionSources(), CommunityCodeValues.ACCESSION_SOURCE_TYPE_COLLECTED))
.setCollectingMissionIdentifier(mcpd.collMissid)
.setCollectingNumber(mcpd.collNumb)
.setCollectingSite(new Collsite()
// .setCoordinateUncertainty(mcpd.coordUncert) // special formatting
// .setElevation(mcpd.elevation) // special formatting
.setGeoreferencingMethod(mcpd.geoRefMeth)
// .setLatitudeDecimal(mcpd.decLatitude) // special formatting
.setLatitudeDegrees(null)
// .setLongitudeDecimal(mcpd.decLongitude) // special formatting
.setLongitudeDegrees(null)
.setSpatialReferenceSystem(mcpd.coordDatum)
)
);
if (mcpd.decLatitude != null) dto.getCollectingInfo().getCollectingSite().setLatitudeDecimal(df.get().format(mcpd.decLatitude));
if (mcpd.decLongitude != null) dto.getCollectingInfo().getCollectingSite().setLongitudeDecimal(df.get().format(mcpd.decLongitude));
if (mcpd.coordUncert != null) dto.getCollectingInfo().getCollectingSite().setCoordinateUncertainty(df.get().format(mcpd.coordUncert));
if (mcpd.elevation != null) dto.getCollectingInfo().getCollectingSite().setElevation(df.get().format(mcpd.elevation));
dto.setCommonCropName(mcpd.cropName);
dto.setCountryOfOrigin(mcpd.origCty);
dto.setDonorInfo(new McpdDonor()
.setDonorAccessionNumber(mcpd.donorNumb)
// .setDonorAccessionPui(null)
.setDonorInstitute(toBrapiInstitute(a.getAccessionSources(), CommunityCodeValues.ACCESSION_SOURCE_TYPE_DONATED))
);
if (mcpd.mlsStat != null) dto.setMlsStatus(mcpd.mlsStat.toString());
dto.setRemarks(mcpd.remarks);
dto.setSafetyDuplicateInstitutes(
Arrays.asList(a.getBackupLocation1Site(), a.getBackupLocation2Site()).stream()
// filter
.filter(s -> s != null)
// map
.map(site -> new Institute()
.setInstituteCode(site.getFaoInstituteNumber())
.setInstituteName(site.getSiteLongName())
// .setInstituteAddress()
)
.collect(Collectors.toList())
);
if (StringUtils.isNotBlank(mcpd.storage)) dto.setStorageTypeCodes(Arrays.asList(mcpd.storage.split(";")));
return dto;
}
private static Institute toBrapiInstitute(List<AccessionSource> accessionSources, CodeValueDef sourceType) {
var institutes = toBrapiInstitutes(accessionSources, sourceType);
return CollectionUtils.isEmpty(institutes) ? null : institutes.get(0);
}
private static List<Institute> toBrapiInstitutes(List<AccessionSource> accessionSources, CodeValueDef sourceType) {
if (CollectionUtils.isEmpty(accessionSources)) return null;
return filterSource(accessionSources, sourceType)
// map
.map(AccessionSource::getCooperators)
// reduce
.flatMap(List::stream)
.map(coop -> new Institute()
.setInstituteName(coop.getOrganization())
.setInstituteCode(coop.getFaoInstituteNumber())
.setInstituteAddress(StringUtils.joinWith("\n", coop.getAddressLine1(), coop.getAddressLine2(), coop.getAddressLine3()))
)
.distinct()
.collect(Collectors.toList());
}
private static Stream<AccessionSource> filterSource(List<AccessionSource> accessionSources, CodeValueDef sourceType) {
if (CollectionUtils.isEmpty(accessionSources)) return Stream.ofNullable(null);
return accessionSources.stream()
// filter
.filter(src -> Objects.equals(src.getSourceTypeCode(), sourceType.value));
}
public CropTrait map(Trait trait) {
var dto = new CropTrait();
if (trait.getTraitDbId() != null) dto.setId(Long.parseLong(trait.getTraitDbId()));
dto.setCodedName(trait.getMainAbbreviation());
dto.setCategoryCode(trait.getTraitClass());
return dto;
}
public Trait map(CropTrait trait) {
if (trait == null) return null;
var dto = new Trait();
dto.setTraitDbId(map(trait.getId()));
dto.setTraitName(trait.getCodedName());
// dto.setStatus(trait.getIsPeerReviewed());
dto.setMainAbbreviation(trait.getCodedName());
dto.setTraitClass(trait.getCategoryCode());
dto.setSynonyms(List.of());
return dto;
}
public Trait map(TranslatedCropTrait trait) {
if (trait == null) return null;
var dto = map(trait.entity);
dto.setTraitName(trait.title);
dto.setTraitDescription(trait.description);
return dto;
}
public ObservationVariable mapVariable(CropTrait trait) {
var dto = new ObservationVariable();
dto.setObservationVariableDbId(map(trait.getId()));
dto.setObservationVariableName(trait.getCodedName());
dto.setTrait(map(trait));
dto.setMethod(mapMethod(trait));
dto.setScale(mapScale(trait));
dto.setCommonCropName(trait.getCrop().getName());
// dto.setContextOfUse(null)
// dto.setAdditionalInfo(null)
dto.setSynonyms(List.of()); // NPE in FieldBook
return dto;
}
public ObservationVariable mapVariable(TranslatedCropTrait trait) {
var dto = mapVariable(trait.entity);
dto.setTrait(map(trait));
dto.setMethod(mapMethod(trait));
dto.setScale(mapScale(trait));
return dto;
}
private Method mapMethod(TranslatedCropTrait trait) {
if (trait == null) return null;
var dto = mapMethod(trait.entity);
dto.setMethodName(trait.title);
dto.setDescription(trait.description);
return dto;
}
private Method mapMethod(CropTrait trait) {
var dto = new Method();
dto.setMethodDbId(map(trait.getId()));
dto.setMethodName(trait.getCodedName());
dto.setMethodClass(trait.getCategoryCode());
return dto;
}
private Scale mapScale(CropTrait trait) {
if (trait == null) return null;
var dto = new Scale();
dto.setScaleDbId(map(trait.getId()));
dto.setDataType(mapDataType(trait));
// dto.setDecimalPlaces(trait.)
dto.setScaleName(trait.getCodedName());
// dto.setUnits(trait.getUom());
if (Objects.equals("Y", trait.getIsCoded())) dto.setValidValues(mapValidValues(trait));
return dto;
}
private Scale mapScale(TranslatedCropTrait trait) {
if (trait == null) return null;
var dto = mapScale(trait.entity);
dto.setScaleName(trait.title);
if (Objects.equals("Y", trait.entity.getIsCoded())) dto.setValidValues(mapValidValues(trait));
return dto;
}
private DataType mapDataType(CropTrait trait) {
if (Objects.equals("Y", trait.getIsCoded())) {
return Scale.DataType.NOMINAL;
}
String dataTypeCode = trait.getDataTypeCode().toUpperCase().trim();
switch (dataTypeCode) {
case CommunityCodeValues.DATATYPE_NUMERIC:
return Scale.DataType.NUMERICAL;
case CommunityCodeValues.DATATYPE_CHAR:
return Scale.DataType.TEXT;
default:
return Scale.DataType.TEXT;
}
}
private ValidValues mapValidValues(CropTrait trait) {
var dto = new ValidValues();
if (Objects.equals("Y", trait.getIsCoded())) {
dto.setCategories(map(trait.getCodes(), this::map));
}
dto.setMinimumValue(map(trait.getNumericMinimum()));
dto.setMaximumValue(map(trait.getNumericMaximum()));
if (trait.getNumericMinimum() != null) dto.setMin(trait.getNumericMinimum().intValue());
if (trait.getNumericMaximum() != null) dto.setMax(trait.getNumericMaximum().intValue());
return dto;
}
private ValidValues mapValidValues(TranslatedCropTrait trait) {
var dto = mapValidValues(trait.entity);
if (Objects.equals("Y", trait.entity.getIsCoded())) {
dto.setCategories(map(trait.codes, this::map));
}
return dto;
}
private Category map(CropTraitCode data) {
var dto = new Category();
dto.setValue(data.getCode());
dto.setLabel(data.getCode());
return dto;
}
private Category map(TranslatedCropTraitCode data) {
var dto = new Category();
dto.setValue(data.entity.getCode());
dto.setLabel(data.title);
return dto;
}
public CropTrait map(ObservationVariable variable) {
return null;
}
}