BrAPIv2FacadeImpl.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.impl;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.apache.commons.lang3.StringUtils;
import org.genesys.blocks.model.filters.StringFilter;
import org.gringlobal.api.exception.InvalidApiUsageException;
import org.gringlobal.brapi.v2.BrAPIv2Facade;
import org.gringlobal.brapi.v2.BrAPIv2MapperX;
import org.gringlobal.brapi.v2.query.ObservationSearchQuery;
import org.gringlobal.brapi.v2.query.ObservationUnitSearchQuery;
import org.gringlobal.brapi.v2.query.ProgramSearchQuery;
import org.gringlobal.brapi.v2.query.StudySearchQuery;
import org.gringlobal.brapi.v2.query.TrialSearchQuery;
import org.gringlobal.custom.elasticsearch.SearchException;
import org.gringlobal.model.Crop;
import org.gringlobal.model.CropTraitObservation;
import org.gringlobal.model.community.CommunityCodeValues;
import org.gringlobal.service.AccessionService;
import org.gringlobal.service.CodeValueService;
import org.gringlobal.service.CropService;
import org.gringlobal.service.CropTraitObservationService;
import org.gringlobal.service.CropTraitService;
import org.gringlobal.service.InventoryService;
import org.gringlobal.service.MethodService;
import org.gringlobal.service.SiteService;
import org.gringlobal.service.filter.AccessionFilter;
import org.gringlobal.service.filter.CodeValueFilter;
import org.gringlobal.service.filter.CropTraitFilter;
import org.gringlobal.service.filter.CropTraitObservationFilter;
import org.gringlobal.service.filter.MethodFilter;
import org.gringlobal.service.filter.SiteFilter;
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.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.extern.slf4j.Slf4j;
import uk.ac.hutton.ics.brapi.resource.core.program.Program;
import uk.ac.hutton.ics.brapi.resource.core.program.ProgramSearch;
import uk.ac.hutton.ics.brapi.resource.core.study.Study;
import uk.ac.hutton.ics.brapi.resource.core.study.StudySearch;
import uk.ac.hutton.ics.brapi.resource.core.trial.Trial;
import uk.ac.hutton.ics.brapi.resource.core.trial.TrialSearch;
import uk.ac.hutton.ics.brapi.resource.germplasm.attribute.Trait;
import uk.ac.hutton.ics.brapi.resource.germplasm.germplasm.Germplasm;
import uk.ac.hutton.ics.brapi.resource.germplasm.germplasm.Mcpd;
import uk.ac.hutton.ics.brapi.resource.phenotyping.observation.Observation;
import uk.ac.hutton.ics.brapi.resource.phenotyping.observation.ObservationSearch;
import uk.ac.hutton.ics.brapi.resource.phenotyping.observation.ObservationUnit;
import uk.ac.hutton.ics.brapi.resource.phenotyping.observation.ObservationVariable;
/**
* Implementation to get Traits information
*/
@Service
@Slf4j
public class BrAPIv2FacadeImpl implements BrAPIv2Facade {
// private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(BrAPIv2FacadeImpl.class);
@Autowired
@Lazy
private CodeValueService codeValueService;
@Autowired
@Lazy
private CropTraitService cropTraitService;
@Autowired
@Lazy
private CropService cropService;
@Autowired
@Lazy
private AccessionService accessionService;
@Autowired
@Lazy
private InventoryService inventoryService;
@Autowired
@Lazy
private MethodService methodService;
@Autowired
@Lazy
private SiteService siteService;
@Autowired
@Lazy
private CropTraitObservationService cropTraitObservationService;
@Autowired
@Lazy
private BrAPIv2MapperX brAPIv2Mapper;
@PersistenceContext
private EntityManager entityManager;
@Override
public Page<String> getCommonCropNames(Pageable page) {
return cropService.list(page).map(Crop::getName);
}
@Override
public Page<String> getStudyTypes(Pageable page) throws Exception {
var filter = new CodeValueFilter()
.groupName(new StringFilter().eq(Set.of(CommunityCodeValues.METHOD_STUDY_TYPE)));
return BrAPIv2MapperX.map(codeValueService.listFiltered(filter, page), tcv -> StringUtils.defaultIfBlank(tcv.title, tcv.entity.getValue()));
}
@Override
@Transactional(readOnly = true)
public Page<Germplasm> listAccessions(AccessionFilter filter, Pageable page) throws SearchException {
// if (filter.isEmpty()) {
// log.warn("Filter is empty");
// return Page.empty();
// }
return BrAPIv2MapperX.map(accessionService.list(filter, page), brAPIv2Mapper::map);
}
@Override
@Transactional(readOnly = true)
public Germplasm getAccession(long germplasmDbId) {
return brAPIv2Mapper.map(accessionService.get(germplasmDbId));
}
@Override
@Transactional(readOnly = true)
public Mcpd getAccessionMcpd(long germplasmDbId) {
return brAPIv2Mapper.map(accessionService.getMCPD(germplasmDbId));
}
// @Override
// @Transactional(readOnly = true)
// public Page<Germplasm> listInventories(InventoryFilter filter, Pageable page) throws SearchException {
// if (filter.isEmpty()) {
// log.warn("Filter is empty");
// return Page.empty();
// }
// return BrAPIv2MapperX.map(inventoryService.list(filter, page), brAPIv2Mapper::map);
// }
// @Override
// @Transactional(readOnly = true)
// public Germplasm getInventory(long germplasmDbId) {
// return brAPIv2Mapper.map(inventoryService.get(germplasmDbId));
// }
// @Override
// @Transactional
// public Page<Germplasm> createAccessions(Germplasm... accessions) throws SearchException {
// var created = accessionService.create(BrAPIv2MapperX.map(Arrays.asList(accessions), brAPIv2Mapper::map)).success;
// return new PageImpl<>(created).map(brAPIv2Mapper::map);
// }
// @Override
// @Transactional
// public Germplasm updateAccession(long germplasmDbId, Germplasm accession) throws Exception {
// var target = accessionService.get(germplasmDbId);
// return brAPIv2Mapper.map(accessionService.update(brAPIv2Mapper.map(accession), target));
// }
/*
* Traits
*/
@Override
@Transactional(readOnly = true)
public Page<Trait> listTraits(CropTraitFilter filter, Pageable page) throws SearchException {
return BrAPIv2MapperX.map(cropTraitService.listFiltered(filter, page), brAPIv2Mapper::mapTrait);
}
@Override
@Transactional(readOnly = true)
public Trait getTrait(long traitDbId) {
return brAPIv2Mapper.mapTrait(cropTraitService.loadTranslated(traitDbId));
}
// @Override
// @Transactional
// public Trait updateTrait(long traitDbId, Trait trait) {
// var target = cropTraitService.get(traitDbId);
// return brAPIv2Mapper.mapVariable(cropTraitService.update(brAPIv2Mapper.mapTrait(trait), target));
// }
// @Override
// @Transactional
// public Page<Trait> createTraits(Trait... traits) {
// var created = cropTraitService.create(brAPIv2Mapper.map(Arrays.asList(traits), brAPIv2Mapper::map)).success;
// return new PageImpl<>(created).mapVariable(brAPIv2Mapper::mapVariable);
// }
/*
* Observation Variables
*/
@Override
@Transactional(readOnly = true)
public Page<ObservationVariable> listObservationVariables(CropTraitFilter filter, Pageable page) throws Exception {
return BrAPIv2MapperX.map(cropTraitService.listFiltered(filter, page), brAPIv2Mapper::mapVariable);
}
@Override
@Transactional(readOnly = true)
public ObservationVariable getObservationVariable(long observationVariableDbId) {
return brAPIv2Mapper.mapVariable(cropTraitService.loadTranslated(observationVariableDbId));
}
// @Override
// @Transactional
// public Page<ObservationVariable> createObservationVariables(ObservationVariable... variables) {
// var created = cropTraitService.create(brAPIv2Mapper.map(Arrays.asList(variables), brAPIv2Mapper::mapVariable)).success;
// return new PageImpl<>(created).mapVariable(brAPIv2Mapper::mapVariable);
// }
// @Override
// @Transactional
// public ObservationVariable updateObservationVariable(long observationVariableDbId, ObservationVariable variable) {
// var target = cropTraitService.get(observationVariableDbId);
// return brAPIv2Mapper.mapVariable(cropTraitService.update(brAPIv2Mapper.mapVariable(variable), target));
// }
@Override
@Transactional(readOnly = true)
public Study getStudy(long id) {
var method = methodService.get(id);
return brAPIv2Mapper.mapStudy(method);
}
@Override
@Transactional(readOnly = true)
public Page<Study> listStudies(StudySearchQuery request, Pageable page) throws Exception {
MethodFilter methodFilter = brAPIv2Mapper.map(request);
var methods = methodService.list(methodFilter, page);
return BrAPIv2MapperX.map(methods, brAPIv2Mapper::mapStudy);
}
@Override
@Transactional(readOnly = true)
public Page<Study> searchStudies(StudySearch request, Pageable page) throws Exception {
MethodFilter methodFilter = brAPIv2Mapper.map(request);
var methods = methodService.list(methodFilter, page);
return BrAPIv2MapperX.map(methods, brAPIv2Mapper::mapStudy);
}
@Override
@Transactional(readOnly = true)
public Trial getTrial(long id) {
var method = methodService.get(id);
return brAPIv2Mapper.mapTrial(method);
}
@Override
@Transactional(readOnly = true)
public Page<Trial> listTrials(TrialSearchQuery request, Pageable page) throws Exception {
MethodFilter methodFilter = brAPIv2Mapper.map(request);
var methods = methodService.list(methodFilter, page);
return BrAPIv2MapperX.map(methods, brAPIv2Mapper::mapTrial);
}
@Override
@Transactional(readOnly = true)
public Page<Trial> searchTrials(TrialSearch request, Pageable page) throws Exception {
MethodFilter methodFilter = brAPIv2Mapper.map(request);
var methods = methodService.list(methodFilter, page);
return BrAPIv2MapperX.map(methods, brAPIv2Mapper::mapTrial);
}
@Override
@Transactional(readOnly = true)
public Program getProgram(long id) {
var site = siteService.get(id);
return brAPIv2Mapper.map(site);
}
@Override
@Transactional(readOnly = true)
public Page<Program> listPrograms(ProgramSearchQuery request, Pageable page) throws Exception {
SiteFilter siteFilter = brAPIv2Mapper.map(request);
var sites = siteService.list(siteFilter, page);
return BrAPIv2MapperX.map(sites, brAPIv2Mapper::map);
}
@Override
@Transactional(readOnly = true)
public Page<Program> searchPrograms(ProgramSearch request, Pageable page) throws Exception {
SiteFilter siteFilter = brAPIv2Mapper.map(request);
var sites = siteService.list(siteFilter, page);
return BrAPIv2MapperX.map(sites, brAPIv2Mapper::map);
}
@Override
@Transactional(readOnly = true)
public Page<ObservationUnit> listObservationUnits(ObservationUnitSearchQuery query, Pageable pageable) throws SearchException {
if (query.getIncludeObservations() != null) {
log.warn("Include observations: {}", query.getIncludeObservations());
}
if (query.getStudyDbId() != null) {
return BrAPIv2MapperX.map(cropTraitObservationService.getObservationInventoriesByMethod(query.getStudyDbId(), pageable), brAPIv2Mapper::mapUnit);
}
if (query.getTrialDbId() != null) {
return BrAPIv2MapperX.map(cropTraitObservationService.getObservationInventoriesByMethod(query.getTrialDbId(), pageable), brAPIv2Mapper::mapUnit);
}
log.warn("Filter does not have studyDbId or trialDbId");
return Page.empty();
}
@Override
@Transactional(readOnly = true)
public Observation getObservation(long id) {
var observation = cropTraitObservationService.load(id);
return brAPIv2Mapper.map(observation);
}
@Override
@Transactional(readOnly = true)
public Page<Observation> listObservations(ObservationSearchQuery request, Pageable page) throws Exception {
CropTraitObservationFilter observationFilter = brAPIv2Mapper.map(request);
if (observationFilter.isEmpty()) {
log.warn("Filter is empty");
return Page.empty();
}
var observations = cropTraitObservationService.list(observationFilter, page);
return BrAPIv2MapperX.map(observations, brAPIv2Mapper::map);
}
@Override
@Transactional(readOnly = true)
public Page<Observation> searchObservations(ObservationSearch request, Pageable page) throws Exception {
CropTraitObservationFilter observationFilter = brAPIv2Mapper.map(request);
if (observationFilter.isEmpty()) {
log.warn("Filter is empty");
return Page.empty();
}
var observations = cropTraitObservationService.list(observationFilter, page);
return BrAPIv2MapperX.map(observations, brAPIv2Mapper::map);
}
@Override
@Transactional
public List<Observation> createObservations(List<Observation> observations) {
// WARNING: Need to keep return externalReferences back to FieldBook
var mappingBrAPI = new LinkedHashMap<CropTraitObservation, Observation>(observations.size());
var mappingGgce = new LinkedHashMap<CropTraitObservation, CropTraitObservation>(observations.size());
return observations.stream()
// Convert BrAPI Observation to CropTraitObservation
.map(o -> {
var cto = brAPIv2Mapper.map(o);
mappingBrAPI.put(cto, o);
return cto;
})
// Update value
.peek(this::updateCropTraitObservationValue)
// Create new CropTraitObservation
.map(cto -> {
var created = cropTraitObservationService.createFast(cto);
entityManager.flush();
mappingGgce.put(created, cto);
return created;
})
// Convert CropTraitObservation to BrAPI Observation
.map(cto -> {
// Need to set externalReferences back
var o = brAPIv2Mapper.map(cto);
o.setExternalReferences(mappingBrAPI.get(mappingGgce.get(cto)).getExternalReferences());
return o;
}).collect(Collectors.toList());
}
@Override
@Transactional
public Observation updateObservation(long observationDbId, Observation observation) {
return updateObservations(Map.of(observationDbId, observation)).get(0);
}
@Override
@Transactional
public List<Observation> updateObservations(Map<Long, Observation> observationsById) {
// WARNING: Need to keep return externalReferences back to FieldBook
var mappingBrAPI = new LinkedHashMap<CropTraitObservation, Observation>(observationsById.size());
var mappingGgce = new LinkedHashMap<CropTraitObservation, CropTraitObservation>(observationsById.size());
return observationsById.entrySet().stream().map(kv -> {
var cto = brAPIv2Mapper.map(kv.getValue());
cto.setId(kv.getKey());
mappingBrAPI.put(cto, kv.getValue());
return cto;
})
// Fix value
.peek(this::updateCropTraitObservationValue)
// Update
.map(cto -> {
var created = cropTraitObservationService.forceUpdate(cto);
entityManager.flush();
mappingGgce.put(created, cto);
return created;
})
// CTO -> Observation
.map(cto -> {
// Need to set externalReferences back
var o = brAPIv2Mapper.map(cto);
o.setExternalReferences(mappingBrAPI.get(mappingGgce.get(cto)).getExternalReferences());
return o;
}).collect(Collectors.toList());
}
private void updateCropTraitObservationValue(CropTraitObservation cto) {
var ct = cto.getCropTrait();
if (ct == null) {
throw new InvalidApiUsageException("No such CropTrait");
}
if (ct.getCodes().size() > 0) {
// Find code for originalValue
var codeForValue = ct.getCodes().stream().filter(ctc -> Objects.equals(ctc.getCode(), cto.getOriginalValue())).findFirst();
if (codeForValue.isPresent()) {
cto.setCropTraitCode(codeForValue.get());
} else {
if (Objects.equals(cto.getOriginalValue(), "NA")) {
// "NA" is used by Fieldbook as null value
cto.setCropTraitCode(null);
return;
}
throw new InvalidApiUsageException("No CropTraitCode for value " + cto.getOriginalValue());
}
} else if (Objects.equals(CommunityCodeValues.CROP_TRAIT_DATA_TYPE_NUMERIC.value, ct.getDataTypeCode())) {
// Numeric
cto.setNumericValue(Double.parseDouble(cto.getOriginalValue()));
} else {
cto.setStringValue(cto.getOriginalValue());
}
}
}