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());
		}
	}
}