BrAPIv2MapperX.java

/*
 * Copyright 2024 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.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.genesys.blocks.security.SecurityContextUtil;
import org.gringlobal.brapi.v2.query.GermplasmQuery;
import org.gringlobal.brapi.v2.query.ObservationSearchQuery;
import org.gringlobal.brapi.v2.query.ProgramSearchQuery;
import org.gringlobal.brapi.v2.query.StudySearchQuery;
import org.gringlobal.brapi.v2.query.TrialSearchQuery;
import org.gringlobal.model.Accession;
import org.gringlobal.model.CropTrait;
import org.gringlobal.model.CropTraitCode;
import org.gringlobal.model.CropTraitObservation;
import org.gringlobal.model.Inventory;
import org.gringlobal.model.Site;
import org.gringlobal.model.community.AccessionMCPD;
import org.gringlobal.model.community.CommunityCodeValues;
import org.gringlobal.service.CropTraitCodeTranslationService.TranslatedCropTraitCode;
import org.gringlobal.service.CropTraitService;
import org.gringlobal.service.CropTraitTranslationService.TranslatedCropTrait;
import org.gringlobal.service.InventoryService;
import org.gringlobal.service.MethodService;
import org.gringlobal.service.SiteService;
import org.gringlobal.service.TaxonomySpeciesCRUDService;
import org.gringlobal.service.filter.AccessionFilter;
import org.gringlobal.service.filter.CropTraitObservationFilter;
import org.gringlobal.service.filter.MethodFilter;
import org.gringlobal.service.filter.SiteFilter;
import org.mapstruct.BeanMapping;
import org.mapstruct.InjectionStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.domain.Page;
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.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.Trait;
import uk.ac.hutton.ics.brapi.resource.germplasm.germplasm.Germplasm;
import uk.ac.hutton.ics.brapi.resource.germplasm.germplasm.GermplasmSearch;
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;

/**
 * GGCE to BrAPIv2
 */
@Mapper(componentModel = "spring", injectionStrategy = InjectionStrategy.FIELD, imports = { Objects.class, SecurityContextUtil.class })
@Slf4j
public abstract class BrAPIv2MapperX {

	protected final static String Y = "Y";
	protected final static String N = "N";
	protected final ZoneId UTC =  ZoneId.of("UTC");

	@Autowired
	@Lazy
	private SiteService siteService;

	@Autowired
	@Lazy
	private TaxonomySpeciesCRUDService taxonomySpeciesService;

	@Autowired
	@Lazy
	private MethodService methodService;

	@Autowired
	@Lazy
	private CropTraitService cropTraitService;

	@Autowired
	@Lazy
	private InventoryService inventoryService;


	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) {
		if (source != null) {
			log.debug("Mapping page size={} content={}", source.getSize(), source.getContent().size());
		}
		return source.map(mapper);
	}

	/** Convert single string to a list with one element */
	public List<String> mapToList(String value) {
		if (value == null) return null;
		return List.of(value);
	}

	/** Convert single string to a set with one element */
	public Set<Long> mapToSet(String value) {
		if (value == null) return null;
		return Set.of(Long.parseLong(value));
	}

	/** Convert single string to a set with one element */
	public Set<String> mapToStringSet(String value) {
		if (value == null) return null;
		return Set.of(value);
	}

	/** Convert value to URI */
	public URI mapToUri(String value) {
		try {
			return value == null ? null : new URI(value);
		} catch (URISyntaxException e) {
			return null;
		}
	}

	public String map(Date date) {
		return date == null ? null : DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(Instant.ofEpochMilli(date.getTime()).atZone(UTC));
	}

	public String map(Instant instant) {
		return instant == null ? null : DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(instant.atZone(UTC));
	}

	public String map(LocalDateTime instant) {
		return instant == null ? null : DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(instant);
	}

	public String map(LocalDate instant) {
		return instant == null ? null : DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(instant);
	}

	@Mapping(target = "programDbId", source = "id")
	@Mapping(target = "programName", source = "siteLongName")
	@Mapping(target = "abbreviation", source = "siteShortName")
	@Mapping(target = "programType", source = "typeCode")
	@Mapping(target = "additionalInfo", ignore = true)
	@Mapping(target = "commonCropName", ignore = true)
	@Mapping(target = "documentationURL", ignore = true)
	@Mapping(target = "externalReferences", ignore = true)
	@Mapping(target = "fundingInformation", ignore = true)
	@Mapping(target = "leadPersonDbId", ignore = true)
	@Mapping(target = "leadPersonName", ignore = true)
	@Mapping(target = "objective", ignore = true)
	public abstract Program map(Site s);

	@BeanMapping(ignoreByDefault = true)
	@Mapping(target = "id", source = "programDbId")
	@Mapping(target = "siteShortName.eq", source = "abbreviation")
	@Mapping(target = "typeCode", source = "programType")
	@Mapping(target = "siteLongName.eq", source = "programName")
	public abstract SiteFilter map(ProgramSearchQuery request);

	@BeanMapping(ignoreByDefault = true)
	@Mapping(target = "siteShortName.eq", source = "abbreviations")
	@Mapping(target = "id", source = "programDbIds")
	@Mapping(target = "typeCode", source = "programTypes")
	@Mapping(target = "siteLongName.eq", source = "programNames")
	public abstract SiteFilter map(ProgramSearch request);



	@Mapping(target = "germplasmDbId", source = "id")
	@Mapping(target = "germplasmPUI", source = "doi")
	@Mapping(target = "instituteCode", source = "site.faoInstituteNumber")
	@Mapping(target = "instituteName", source = "site.siteLongName")
	@Mapping(target = "accessionNumber", source = "accessionNumber")
	@Mapping(target = "germplasmName", source = "preferredName")
	@Mapping(target = "defaultDisplayName", source = "accessionNumber")
	// @Mapping(target = "acquisitionDate", source = "initialReceivedDate") // FieldBook accepts only LocalDate
	@Mapping(target = "genus", source = "taxonomySpecies.taxonomyGenus.name")
	@Mapping(target = "species", source = "taxonomySpecies.specificEpithet")
	@Mapping(target = "speciesAuthority", source = "taxonomySpecies.speciesAuthority")
	@Mapping(target = "subtaxa", source = "taxonomySpecies.subTaxon")
	@Mapping(target = "subtaxaAuthority", source = "taxonomySpecies.nameAuthority")
	@Mapping(target = "additionalInfo", ignore = true)
	@Mapping(target = "biologicalStatusOfAccessionCode", source = "improvementStatusCode")
	@Mapping(target = "biologicalStatusOfAccessionDescription", source = "improvementStatusCode")
	@Mapping(target = "breedingMethodDbId", ignore = true)
	@Mapping(target = "breedingMethodName", ignore = true)
	@Mapping(target = "collection", ignore = true)
	@Mapping(target = "commonCropName", expression = "java(a.getTaxonomySpecies().reportCropName())")
	@Mapping(target = "countryOfOriginCode", ignore = true)
	@Mapping(target = "documentationURL", ignore = true)
	@Mapping(target = "donors", ignore = true)
	@Mapping(target = "externalReferences", ignore = true)
	@Mapping(target = "germplasmOrigin", ignore = true)
	@Mapping(target = "germplasmPreprocessing", ignore = true)
	@Mapping(target = "pedigree", ignore = true)
	@Mapping(target = "seedSource", ignore = true)
	@Mapping(target = "seedSourceDescription", ignore = true)
	@Mapping(target = "storageTypes", ignore = true)
	@Mapping(target = "synonyms", ignore = true)
	@Mapping(target = "taxonIds", ignore = true)
	public abstract Germplasm map(Accession a);


// 	public Accession map(Germplasm germplasm) {
// 		var dto = new Accession();
// 		if (germplasm.getGermplasmDbId() != null) dto.setId(Long.parseLong(germplasm.getGermplasmDbId()));
// 		dto.setAccessionNumber(germplasm.getAccessionNumber());
// 		dto.setAccessionNumberPart1(germplasm.getAccessionNumber()); // Can we split this?
// 		if (germplasm.getAcquisitionDate() != null) {
// 			var receivedDate = LocalDate.parse(germplasm.getAcquisitionDate(), DateTimeFormatter.ISO_LOCAL_DATE);
// 			dto.setInitialReceivedDate(Date.from(receivedDate.atStartOfDay().atOffset(ZoneOffset.UTC).toInstant()));
// 			dto.setInitialReceivedDateCode(CommunityCodeValues.DATE_FORMAT_DATE.value);
// 		}
// //		dto.setPreferredName(a.getGermplasmName());
// 		dto.setDoi(germplasm.getGermplasmPUI());

// 		// Site
// 		var siteFilter = new SiteFilter()
// 			.faoInstituteNumber(new StringFilter().eq(Set.of(germplasm.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(germplasm.getGenus(), germplasm.getSpecies(), germplasm.getSpeciesAuthority(), germplasm.getSubtaxa(), germplasm.getSubtaxaAuthority());
// 		dto.setTaxonomySpecies(taxonomySpecies);

// 		return dto;
// 	}

	// @Mapping(target = "germplasmDbId", source = "id")
	// @Mapping(target = "germplasmPUI", source = "doi")
	// @Mapping(target = "instituteCode", source = "site.faoInstituteNumber")
	// @Mapping(target = "accessionNumber", source = "accession.accessionNumber")
	// // @Mapping(target = "acquisitionDate", source = "accession.initialReceivedDate")
	// @Mapping(target = "defaultDisplayName", source = "inventoryNumber")
	// @Mapping(target = "germplasmName", source = "preferredName")
	// @Mapping(target = "genus", source = "accession.taxonomySpecies.taxonomyGenus.name")
	// @Mapping(target = "species", source = "accession.taxonomySpecies.specificEpithet")
	// @Mapping(target = "speciesAuthority", source = "accession.taxonomySpecies.speciesAuthority")
	// @Mapping(target = "subtaxa", source = "accession.taxonomySpecies.subTaxon")
	// @Mapping(target = "subtaxaAuthority", source = "accession.taxonomySpecies.nameAuthority")
	// @Mapping(target = "additionalInfo", ignore = true)
	// @Mapping(target = "biologicalStatusOfAccessionCode", source = "accession.improvementStatusCode")
	// @Mapping(target = "biologicalStatusOfAccessionDescription", source = "accession.improvementStatusCode")
	// @Mapping(target = "breedingMethodDbId", ignore = true)
	// @Mapping(target = "breedingMethodName", ignore = true)
	// @Mapping(target = "collection", ignore = true)
	// @Mapping(target = "commonCropName", expression = "java(i.getAccession().getTaxonomySpecies().reportCropName())")
	// @Mapping(target = "countryOfOriginCode", ignore = true)
	// @Mapping(target = "documentationURL", ignore = true)
	// @Mapping(target = "donors", ignore = true)
	// @Mapping(target = "externalReferences", ignore = true)
	// @Mapping(target = "germplasmOrigin", ignore = true)
	// @Mapping(target = "germplasmPreprocessing", ignore = true)
	// @Mapping(target = "instituteName", ignore = true)
	// @Mapping(target = "pedigree", ignore = true)
	// @Mapping(target = "seedSource", ignore = true)
	// @Mapping(target = "seedSourceDescription", ignore = true)
	// @Mapping(target = "storageTypes", ignore = true)
	// @Mapping(target = "synonyms", ignore = true)
	// @Mapping(target = "taxonIds", ignore = true)
	// public abstract Germplasm map(Inventory i);


	@Mapping(target = "germplasmDbId", source = "id")
	@Mapping(target = "germplasmPUI", source = "puid")
	@Mapping(target = "instituteCode", source = "instCode")
	@Mapping(target = "accessionNumber", source = "acceNumb")
	@Mapping(target = "alternateIDs", source = "otherNumb")
	@Mapping(target = "acquisitionDate", source = "acqDate")
	@Mapping(target = "biologicalStatusOfAccessionCode", source = "sampStat")
	@Mapping(target = "acquisitionSourceCode", ignore = true)
	@Mapping(target = "accessionNames", source = "acceName")
	@Mapping(target = "ancestralData", source = "ancest")
	@Mapping(target = "commonCropName", source = "cropName")
	@Mapping(target = "countryOfOrigin", source = "origCty")
	@Mapping(target = "mlsStatus", source = "mlsStat")
	@Mapping(target = "speciesAuthority", source = "spAuthor")
	@Mapping(target = "subtaxon", source = "subtaxa")
	@Mapping(target = "subtaxonAuthority", source = "subtAuthor")
	@Mapping(target = "storageTypeCodes", source = "storage")
	@Mapping(target = "collectingInfo.collectingDate", source = "collDate")
	@Mapping(target = "collectingInfo.collectingNumber", source = "collNumb")
	@Mapping(target = "collectingInfo.collectingMissionIdentifier", source = "collMissid")
	@Mapping(target = "collectingInfo.collectingSite.latitudeDecimal", source = "decLatitude")
	@Mapping(target = "collectingInfo.collectingSite.longitudeDecimal", source = "decLongitude")
	@Mapping(target = "collectingInfo.collectingSite.georeferencingMethod", source = "geoRefMeth")
	@Mapping(target = "collectingInfo.collectingSite.spatialReferenceSystem", source = "coordDatum")
	// @Mapping(target = "collectingInfo.collectingInstitutes.instituteCode", source = "collCode")
	// @Mapping(target = "collectingInfo.collectingInstitutes.instituteName", source = "collName")
	@Mapping(target = "donorInfo.donorInstitute.instituteCode", source = "donorCode")
	@Mapping(target = "donorInfo.donorInstitute.instituteName", source = "donorName")
	@Mapping(target = "breedingInstitutes", ignore = true)
	@Mapping(target = "safetyDuplicateInstitutes", ignore = true)
	public abstract Mcpd map(AccessionMCPD mcpd);

	@Mapping(target = "programDbId", ignore = true)
	@Mapping(target = "programName", ignore = true)
	@Mapping(target = "trialDbId", source = "id")
	@Mapping(target = "trialPUI", ignore = true)
	@Mapping(target = "trialName", source = "name")
	@Mapping(target = "commonCropName", ignore = true)
	@Mapping(target = "active", constant = "true")
	@Mapping(target = "startDate", ignore = true)
	@Mapping(target = "endDate", ignore = true)
	@Mapping(target = "trialDescription", source = "materialsAndMethods")
	@Mapping(target = "publications", ignore = true)
	@Mapping(target = "externalReferences", ignore = true)
	@Mapping(target = "documentationURL", ignore = true)
	@Mapping(target = "contacts", ignore = true)
	@Mapping(target = "datasetAuthorships", ignore = true)
	@Mapping(target = "additionalInfo", source = ".", qualifiedByName = "mapInfo")
	public abstract Trial mapTrial(org.gringlobal.model.Method method);

	@Mapping(target = "trialDbId", source = "id")
	@Mapping(target = "studyDbId", source = "id")
	@Mapping(target = "studyName", source = "name")
	@Mapping(target = "studyCode", source = "name")
	@Mapping(target = "studyType", source = "studyReasonCode")
	@Mapping(target = "trialName", source = "name")
	@Mapping(target = "active", constant = "true")
	@Mapping(target = "startDate", ignore = true)
	@Mapping(target = "endDate", ignore = true)
	@Mapping(target = "studyDescription", source = "materialsAndMethods")
	@Mapping(target = "lastUpdate.timestamp", source = "modifiedDate")
	@Mapping(target = "lastUpdate.version", source = "modifiedDate")
	@Mapping(target = "additionalInfo", source = ".", qualifiedByName = "mapInfo")
	@Mapping(target = "commonCropName", ignore = true)
	@Mapping(target = "contacts", ignore = true)
	@Mapping(target = "culturalPractices", ignore = true)
	@Mapping(target = "dataLinks", ignore = true)
	@Mapping(target = "documentationURL", ignore = true)
	@Mapping(target = "environmentParameters", ignore = true)
	@Mapping(target = "experimentalDesign", ignore = true)
	@Mapping(target = "externalReference", ignore = true)
	@Mapping(target = "growthFacility", ignore = true)
	@Mapping(target = "license", ignore = true)
	@Mapping(target = "locationDbId", ignore = true)
	@Mapping(target = "locationName", ignore = true)
	@Mapping(target = "observationLevels", ignore = true)
	@Mapping(target = "observationUnitsDescription", ignore = true)
	@Mapping(target = "observationVariableDbIds", ignore = true)
	@Mapping(target = "seasons", ignore = true)
	@Mapping(target = "studyPUI", ignore = true)
	public abstract Study mapStudy(org.gringlobal.model.Method method);

	@Named("mapInfo")
	public Map<String, String> mapInfo(org.gringlobal.model.Method method) {
		return Map.of(
			// "studyReasonCode", method.getStudyReasonCode()
		);
	}

	@Named("mapTrait")
	@Mapping(target = "traitDbId", source = "id")
	@Mapping(target = "mainAbbreviation", source = "codedName")
	@Mapping(target = "traitName", source = "codedName")
	@Mapping(target = "traitDescription", ignore = true)
	@Mapping(target = "traitClass", source = "categoryCode")
	@Mapping(target = "additionalInfo", ignore = true)
	@Mapping(target = "alternativeAbbreviations", ignore = true)
	@Mapping(target = "attribute", ignore = true)
	@Mapping(target = "attributePUI", ignore = true)
	@Mapping(target = "entity", ignore = true)
	@Mapping(target = "entityPUI", ignore = true)
	@Mapping(target = "externalReferences", ignore = true)
	@Mapping(target = "ontologyReference", ignore = true)
	@Mapping(target = "status", ignore = true)
	@Mapping(target = "synonyms", expression = "java(java.util.List.of())")
	@Mapping(target = "traitPUI", ignore = true)
	public abstract Trait mapTrait(CropTrait ct);

	@Named("mapTrait")
	@Mapping(target = "traitDbId", source = "entity.id")
	@Mapping(target = "mainAbbreviation", source = "entity.codedName")
	@Mapping(target = "traitName", source = "title")
	@Mapping(target = "traitDescription", source = "description")
	@Mapping(target = "traitClass", source = "entity.categoryCode")
	@Mapping(target = "additionalInfo", ignore = true)
	@Mapping(target = "alternativeAbbreviations", ignore = true)
	@Mapping(target = "attribute", ignore = true)
	@Mapping(target = "attributePUI", ignore = true)
	@Mapping(target = "entity", ignore = true)
	@Mapping(target = "entityPUI", ignore = true)
	@Mapping(target = "externalReferences", ignore = true)
	@Mapping(target = "ontologyReference", ignore = true)
	@Mapping(target = "status", ignore = true)
	@Mapping(target = "synonyms", expression = "java(java.util.List.of())")
	@Mapping(target = "traitPUI", ignore = true)
	public abstract Trait mapTrait(TranslatedCropTrait tct);

	@Named("mapMethod")
	@Mapping(target = "methodDbId", source = "id")
	@Mapping(target = "methodName", source = "codedName")
	@Mapping(target = "methodClass", source = "categoryCode")
	@Mapping(target = "description", ignore = true)
	@Mapping(target = "additionalInfo", ignore = true)
	@Mapping(target = "bibliographicalReference", ignore = true)
	@Mapping(target = "externalReferences", ignore = true)
	@Mapping(target = "formula", ignore = true)
	@Mapping(target = "methodPUI", ignore = true)
	@Mapping(target = "ontologyReference", ignore = true)
	public abstract Method mapMethod(CropTrait ct);

	@Named("mapMethod")
	@Mapping(target = "methodDbId", source = "entity.id")
	@Mapping(target = "methodName", source = "title")
	@Mapping(target = "description", source = "description")
	@Mapping(target = "methodClass", source = "entity.categoryCode")
	@Mapping(target = "additionalInfo", ignore = true)
	@Mapping(target = "bibliographicalReference", ignore = true)
	@Mapping(target = "externalReferences", ignore = true)
	@Mapping(target = "formula", ignore = true)
	@Mapping(target = "methodPUI", ignore = true)
	@Mapping(target = "ontologyReference", ignore = true)
	public abstract Method mapMethod(TranslatedCropTrait ct);

	@Named("mapScale")
	@Mapping(target = "scaleDbId", source = "id")
	@Mapping(target = "scaleName", source = "codedName")
	@Mapping(target = "units", ignore = true) // We don't have units?!?
	@Mapping(target = "decimalPlaces", ignore = true) // TODO Review
	@Mapping(target = "validValues.categories", source = "codes")
	@Mapping(target = "validValues.minimumValue", source = "numericMinimum")
	@Mapping(target = "validValues.maximumValue", source = "numericMaximum")
	@Mapping(target = "validValues.min", source = "numericMinimum") // BrAPI 2.0
	@Mapping(target = "validValues.max", source = "numericMaximum") // BrAPI 2.0
	@Mapping(target = "dataType", source = ".", qualifiedByName = "toDataType")
	@Mapping(target = "additionalInfo", ignore = true)
	@Mapping(target = "externalReferences", ignore = true)
	@Mapping(target = "ontologyReference", ignore = true)
	@Mapping(target = "scalePUI", ignore = true)
	public abstract Scale mapScale(CropTrait ct);

	@Named("mapScale")
	@Mapping(target = "scaleDbId", source = "entity.id")
	@Mapping(target = "scaleName", source = "title")
	@Mapping(target = "units", ignore = true) // We don't have units?!?
	@Mapping(target = "decimalPlaces", ignore = true) // TODO Review
	@Mapping(target = "validValues.categories", source = "codes")
	@Mapping(target = "validValues.minimumValue", source = "entity.numericMinimum")
	@Mapping(target = "validValues.maximumValue", source = "entity.numericMaximum")
	@Mapping(target = "validValues.min", source = "entity.numericMinimum") // BrAPI 2.0
	@Mapping(target = "validValues.max", source = "entity.numericMaximum") // BrAPI 2.0
	@Mapping(target = "dataType", source = "entity", qualifiedByName = "toDataType")
	@Mapping(target = "additionalInfo", ignore = true)
	@Mapping(target = "externalReferences", ignore = true)
	@Mapping(target = "ontologyReference", ignore = true)
	@Mapping(target = "scalePUI", ignore = true)
	public abstract Scale mapScale(TranslatedCropTrait tct);

	@Named("toDataType")
	public Scale.DataType toDataType(CropTrait ct) {
		// Code, Date, Duration, Nominal, Numerical, Ordinal, Text
		if (Objects.equals(ct.getIsCoded(), Y)) {
			return Scale.DataType.NOMINAL;
		}
		if (Objects.equals(ct.getDataTypeCode(), CommunityCodeValues.DATATYPE_NUMERIC)) {
			return Scale.DataType.NUMERICAL;
		}
		if (Objects.equals(ct.getDataTypeCode(), CommunityCodeValues.DATATYPE_CHAR)) {
			return Scale.DataType.TEXT;
		}
		log.warn("Mapping {} to TEXT", ct.getDataTypeCode());
		return Scale.DataType.TEXT;
	}

	@Mapping(target = "value", source = "code")
	@Mapping(target = "label", source = "code")
	public abstract Category map(CropTraitCode ctc);

	@Mapping(target = "value", source = "entity.code")
	@Mapping(target = "label", source = "title")
	public abstract Category map(TranslatedCropTraitCode tctc);


	@Mapping(target = "observationUnitDbId", source = "id")
	@Mapping(target = "observationUnitName", source = "inventoryNumber")
	@Mapping(target = "observationUnitPUI", ignore = true)
	@Mapping(target = "observationUnitPosition", ignore = true)
	@Mapping(target = "additionalInfo", ignore = true)
	@Mapping(target = "crossDbId", ignore = true)
	@Mapping(target = "crossName", ignore = true)
	@Mapping(target = "externalReferences", ignore = true)
	@Mapping(target = "germplasmDbId", source = "accession.id")
	@Mapping(target = "germplasmName", source = "accession.accessionNumber")
	@Mapping(target = "locationDbId", ignore = true)
	@Mapping(target = "locationName", ignore = true)
	@Mapping(target = "observations", ignore = true)
	@Mapping(target = "programDbId", ignore = true)
	@Mapping(target = "programName", ignore = true)
	@Mapping(target = "seedLotDbId", source = "id")
	@Mapping(target = "seedLotName", source = "inventoryNumber")
	// @Mapping(target = "studyDbId", source = "method.id")
	// @Mapping(target = "studyName", source = "method.name")
	// @Mapping(target = "treatments", ignore = true)
	// @Mapping(target = "trialDbId", source = "method.id")
	// @Mapping(target = "trialName", source = "method.name")
	@Mapping(target = "studyDbId", ignore = true)
	@Mapping(target = "studyName", ignore = true)
	@Mapping(target = "treatments", ignore = true)
	@Mapping(target = "trialDbId", ignore = true)
	@Mapping(target = "trialName", ignore = true)
 	public abstract ObservationUnit mapUnit(Inventory inventory);

	@Mapping(target = "observationVariableDbId", source = "id")
	@Mapping(target = "observationVariableName", source = "codedName")
	@Mapping(target = "commonCropName", source = "crop.name")
	@Mapping(target = "documentationURL", source = "ontologyUrl")
	@Mapping(target = "institution", constant = "GGCE Crop Trait")
	@Mapping(target = "scale", source = ".", qualifiedByName = "mapScale")
	@Mapping(target = "method", source =".", qualifiedByName = "mapMethod")
	@Mapping(target = "trait", source =".", qualifiedByName = "mapTrait")
	@Mapping(target = "language", ignore = true)
	@Mapping(target = "additionalInfo", ignore = true)
	@Mapping(target = "contextOfUse", ignore = true)
	@Mapping(target = "defaultValue", ignore = true)
	@Mapping(target = "externalReferences", ignore = true)
	@Mapping(target = "growthStage", ignore = true)
	@Mapping(target = "ontologyReference", ignore = true)
	@Mapping(target = "scientist", ignore = true)
	@Mapping(target = "status", ignore = true)
	@Mapping(target = "submissionTimestamp", ignore = true)
	@Mapping(target = "synonyms", expression = "java(java.util.List.of())") // Fieldbook dies if this is null
	public abstract ObservationVariable mapVariable(CropTrait ct);

	@Mapping(target = "observationVariableDbId", source = "entity.id")
	@Mapping(target = "observationVariableName", source = "entity.codedName")
	@Mapping(target = "commonCropName", source = "entity.crop.name")
	@Mapping(target = "documentationURL", source = "entity.ontologyUrl")
	@Mapping(target = "institution", constant = "GGCE Crop Trait")
	@Mapping(target = "scale", source = ".", qualifiedByName = "mapScale")
	@Mapping(target = "method", source =".", qualifiedByName = "mapMethod")
	@Mapping(target = "trait", source =".", qualifiedByName = "mapTrait")
	@Mapping(target = "language", ignore = true)
	@Mapping(target = "additionalInfo", ignore = true)
	@Mapping(target = "contextOfUse", ignore = true)
	@Mapping(target = "defaultValue", ignore = true)
	@Mapping(target = "externalReferences", ignore = true)
	@Mapping(target = "growthStage", ignore = true)
	@Mapping(target = "ontologyReference", ignore = true)
	@Mapping(target = "scientist", ignore = true)
	@Mapping(target = "status", ignore = true)
	@Mapping(target = "submissionTimestamp", ignore = true)
	@Mapping(target = "synonyms", expression = "java(java.util.List.of())") // Fieldbook dies if this is null
	public abstract ObservationVariable mapVariable(TranslatedCropTrait tct);

	@Mapping(target = "observationDbId", source = "id")
	@Mapping(target = "observationTimeStamp", source = "modifiedDate") // Use modified date
	@Mapping(target = "observationUnitDbId", source = "inventory.id")
	@Mapping(target = "observationUnitName", source = "inventory.inventoryNumber")
	@Mapping(target = "germplasmDbId", source = "inventory.accession.id")
	@Mapping(target = "germplasmName", source = "inventory.accession.accessionNumber")
	@Mapping(target = "studyDbId", source = "method.id")
	@Mapping(target = "additionalInfo", source = ".", qualifiedByName = "mapInfo")
	@Mapping(target = "observationVariableDbId", source = "cropTrait.id")
	@Mapping(target = "observationVariableName", source = "cropTrait.codedName")
	@Mapping(target = "uploadedBy", source = "modifiedBy")
	@Mapping(target = "value", source = ".", qualifiedByName = "cropTraitObservationValue")
	@Mapping(target = "season", ignore = true)
	@Mapping(target = "collector", ignore = true)
	@Mapping(target = "externalReferences", ignore = true)
	@Mapping(target = "geoCoordinates", ignore = true)
	public abstract Observation map(CropTraitObservation cto);

	@Named("cropTraitObservationValue")
	public String cropTraitObservationValue(CropTraitObservation cto) {
		switch (toDataType(cto.getCropTrait())) {
			case NOMINAL: return cto.getCropTraitCode() == null ? null : cto.getCropTraitCode().getCode();
			// case ORDINAL: return cto.getNumericValue() == null ? null : Double.toString(cto.getNumericValue());
			case NUMERICAL: return cto.getNumericValue() == null ? null : Double.toString(cto.getNumericValue());
			default: return cto.getStringValue();
		}
	}

	@Named("mapInfo")
	public Map<String, String> mapInfo(CropTraitObservation cto) {
		return Map.of(
			// "originalValue", cto.getOriginalValue()
		);
	}


	protected Inventory mapInventory(String observationUnitDbId) {
		if (StringUtils.isBlank(observationUnitDbId)) return null;
		return inventoryService.get(Long.parseLong(observationUnitDbId));
	}
	
	protected CropTrait mapCropTrait(String observationVariableDbId) {
		if (StringUtils.isBlank(observationVariableDbId)) return null;
		return cropTraitService.get(Long.parseLong(observationVariableDbId));
	}
	
	protected org.gringlobal.model.Method mapMethod(String studyDbId) {
		if (StringUtils.isBlank(studyDbId)) return null;
		return methodService.get(Long.parseLong(studyDbId));
	}

	@Mapping(target = "id", source = "observationDbId")
	// @Mapping(target = "modifiedDate", source = "observationTimeStamp") // Don't read
	@Mapping(target = "inventory", source = "observationUnitDbId")
	// @Mapping(target = "inventory.inventoryNumber", source = "observationUnitName")
	// @Mapping(target = "inventory.accession.id", source = "germplasmDbId")
	// @Mapping(target = "inventory.accession.accessionNumber", source = "germplasmName")
	@Mapping(target = "method", source = "studyDbId")
	// @Mapping(target = ".", qualifiedByName = "mapInfo", source = "additionalInfo")
	@Mapping(target = "cropTrait", source = "observationVariableDbId")
	// @Mapping(target = "cropTrait.codedName", source = "observationVariableName")
	@Mapping(target = "modifiedBy", ignore = true)
	@Mapping(target = "originalValue", source = "value")
	// @Mapping(target = true, source = "season")
	// @Mapping(target = true, source = "collector")
	// @Mapping(target = true, source = "externalReferences")
	// @Mapping(target = true, source = "geoCoordinates")
	public abstract CropTraitObservation map(Observation o);

	@BeanMapping(ignoreByDefault = true)
	@Mapping(target = "id", source = "studyDbId")
	@Mapping(target = "studyReasonCode", source = "studyType")
	@Mapping(target = "name.eq", source = "studyName")
	public abstract MethodFilter map(StudySearchQuery request);

	@BeanMapping(ignoreByDefault = true)
	@Mapping(target = "id", source = "studyDbIds")
	@Mapping(target = "studyReasonCode", source = "studyTypes")
	@Mapping(target = "name.eq", source = "studyNames")
	public abstract MethodFilter map(StudySearch request);

	@BeanMapping(ignoreByDefault = true)
	@Mapping(target = "id", source = "trialDbId")
	@Mapping(target = "name.eq", source = "trialName")
	public abstract MethodFilter map(TrialSearchQuery request);

	@BeanMapping(ignoreByDefault = true)
	@Mapping(target = "id", source = "trialDbIds")
	@Mapping(target = "name.eq", source = "trialNames")
	public abstract MethodFilter map(TrialSearch request);

	@BeanMapping(ignoreByDefault = true)
	@Mapping(target = "id", source = "germplasmDbId")
	@Mapping(target = "accessionNumber", source = "accessionNumber")
	@Mapping(target = "doi", source = "germplasmPUI")
	@Mapping(target = "taxonomySpecies.taxonomyGenus.genusName.eq", source = "genus")
	@Mapping(target = "taxonomySpecies.speciesName.eq", source = "species")
	@Mapping(target = "taxonomySpecies.name.sw", source = "binomialName")
	public abstract AccessionFilter filterAccession(GermplasmQuery search);

	@BeanMapping(ignoreByDefault = true)
	@Mapping(target = "id", source = "germplasmDbIds")
	@Mapping(target = "accessionNumber", source = "accessionNumbers")
	@Mapping(target = "site.faoInstituteNumber.eq", source = "instituteCodes")
	@Mapping(target = "doi", source = "germplasmPUIs")
	@Mapping(target = "taxonomySpecies.taxonomyGenus.genusName.eq", source = "genus")
	@Mapping(target = "taxonomySpecies.speciesName.eq", source = "species")
	@Mapping(target = "taxonomySpecies.name.sw", source = "binomialNames")
	public abstract AccessionFilter filterAccession(GermplasmSearch search);

	// @BeanMapping(ignoreByDefault = true)
	// @Mapping(target = "accession.accessionNumber", source = "accessionNumber")
	// @Mapping(target = "accession.doi", source = "germplasmPUI")
	// @Mapping(target = "accession.taxonomySpecies.taxonomyGenus.genusName.eq", source = "genus")
	// @Mapping(target = "accession.taxonomySpecies.speciesName.eq", source = "species")
	// @Mapping(target = "accession.taxonomySpecies.name.eq", source = "binomialName")
	// public abstract InventoryFilter filterInventory(GermplasmQuery search);

	// @BeanMapping(ignoreByDefault = true)
	// @Mapping(target = "accession.accessionNumber", source = "accessionNumbers")
	// @Mapping(target = "accession.site.faoInstituteNumber.eq", source = "instituteCodes")
	// @Mapping(target = "accession.doi", source = "germplasmPUIs")
	// @Mapping(target = "accession.taxonomySpecies.taxonomyGenus.genusName.eq", source = "genus")
	// @Mapping(target = "accession.taxonomySpecies.speciesName.eq", source = "species")
	// @Mapping(target = "accession.taxonomySpecies.name.eq", source = "binomialNames")
	// public abstract InventoryFilter filterInventory(GermplasmSearch search);

	@BeanMapping(ignoreByDefault = true)
	@Mapping(target = "inventory.accession.id", source = "germplasmDbId")
	@Mapping(target = "inventory.id", source = "observationUnitDbId")
	@Mapping(target = "id", source = "observationDbId")
	@Mapping(target = "cropTrait.id", source = "observationVariableDbId")
	@Mapping(target = "cropTrait.codedName", source = "observationVariableName")
	@Mapping(target = "methodId", source = "studyDbId")
	@Mapping(target = "createdDate.ge", source = "observationTimeStampRangeStart")
	@Mapping(target = "createdDate.le", source = "observationTimeStampRangeEnd")
	public abstract CropTraitObservationFilter map(ObservationSearchQuery request);

	@BeanMapping(ignoreByDefault = true)
	@Mapping(target = "inventory.accession.id", source = "germplasmDbIds")
	@Mapping(target = "inventory.id", source = "observationUnitDbIds")
	@Mapping(target = "id", source = "observationDbIds")
	@Mapping(target = "cropTrait.id", source = "observationVariableDbIds")
	@Mapping(target = "cropTrait.codedName", source = "observationVariableNames")
	@Mapping(target = "methodId", source = "studyDbIds")
	@Mapping(target = "createdDate.ge", source = "observationTimeStampRangeStart")
	@Mapping(target = "createdDate.le", source = "observationTimeStampRangeEnd")
	public abstract CropTraitObservationFilter map(ObservationSearch request);


}