diff --git a/src/main/java/io/swagger/api/germ/GermplasmApi.java b/src/main/java/io/swagger/api/germ/GermplasmApi.java index 1990ba83..a7f9d959 100644 --- a/src/main/java/io/swagger/api/germ/GermplasmApi.java +++ b/src/main/java/io/swagger/api/germ/GermplasmApi.java @@ -143,6 +143,7 @@ ResponseEntity germplasmGet( @ApiParam(value = "progenyDbId") @Valid @RequestParam(value = "progenyDbId", required = false) String progenyDbId, @ApiParam(value = "commonCropName") @Valid @RequestParam(value = "commonCropName", required = false) String commonCropName, @ApiParam(value = "programDbId") @Valid @RequestParam(value = "programDbId", required = false) String programDbId, + @ApiParam(value = "programName") @Valid @RequestParam(value = "programName", required = false) String programName, @ApiParam(value = "externalReferenceID") @Valid @RequestParam(value = "externalReferenceID", required = false) String externalReferenceID, @ApiParam(value = "externalReferenceId") @Valid @RequestParam(value = "externalReferenceId", required = false) String externalReferenceId, @ApiParam(value = "externalReferenceSource") @Valid @RequestParam(value = "externalReferenceSource", required = false) String externalReferenceSource, diff --git a/src/main/java/io/swagger/model/FilterBy.java b/src/main/java/io/swagger/model/FilterBy.java new file mode 100644 index 00000000..acbc03e7 --- /dev/null +++ b/src/main/java/io/swagger/model/FilterBy.java @@ -0,0 +1,24 @@ +package io.swagger.model; + +public class FilterBy { + + private String filterOn; + private String value; + + public String getFilterOn() { + return filterOn; + } + + public void setFilterOn(String filterOn) { + this.filterOn = filterOn; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + +} diff --git a/src/main/java/io/swagger/model/SearchRequest.java b/src/main/java/io/swagger/model/SearchRequest.java index 8ad217c4..b59090fe 100644 --- a/src/main/java/io/swagger/model/SearchRequest.java +++ b/src/main/java/io/swagger/model/SearchRequest.java @@ -2,9 +2,15 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.model.sort.SortBy; +import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; +import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; +import org.brapi.test.BrAPITestServer.model.dto.EntityType; +import org.springframework.http.HttpStatus; import java.util.ArrayList; import java.util.List; +import java.util.Map; public abstract class SearchRequest { @JsonIgnore @@ -25,6 +31,20 @@ public abstract class SearchRequest { @JsonProperty("externalReferenceSources") protected List externalReferenceSources = null; + @JsonProperty("filterBy") + protected List filterBy = null; + + @JsonProperty("sortBy") + protected List sortBy = null; + + @JsonIgnore + protected Map sortFilterEntityColumnNamesByRequestName = null; + + @JsonIgnore + public List getExternalReferenceIds() { + return externalReferenceIds; + } + final public SearchRequest page(Integer page) { this.page = page; return this; @@ -119,4 +139,76 @@ public void addExternalReferenceItem(String externalReferenceId, String external } } + + public List getFilterBy() { + return filterBy; + } + + public void setFilterBy(List filterBy) throws BrAPIServerException { + + if (filterBy == null || filterBy.isEmpty()) { + return; + } + + Map allowedSortFilterNames = getEntityColAndTypeBySubmittedNameMap(); + + for (FilterBy filterByItem : filterBy) { + + if (filterByItem.getFilterOn() == null || filterByItem.getFilterOn().isEmpty()) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "filterOn attribute not provided in element of filterBy list."); + } + + if (filterByItem.getValue() == null || filterByItem.getValue().isEmpty()) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "value attribute not provided in element of filterBy list."); + } + + EntityColumnNameAndType entityColumnNameAndType = allowedSortFilterNames.get(filterByItem.getFilterOn()); + + if (entityColumnNameAndType == null) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, + String.format("Supplied filterColumn [%s] not available in allowed names [%s]", filterByItem.getFilterOn(), allowedSortFilterNames.keySet()) + ); + } + + if (entityColumnNameAndType.getEntityType() == EntityType.BOOLEAN) { + // TODO: Add support for this when it becomes relevant for BI or when there is time. + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, String.format("Filtering not implemented for column name [%s] with associated data type [%s]", entityColumnNameAndType.getEntityColumnName(), EntityType.BOOLEAN)); + } + } + this.filterBy = filterBy; + } + + public List getSortByElements() { + return sortBy; + } + + public void setSortBy(List sortBy) throws BrAPIServerException { + + if (sortBy == null || sortBy.isEmpty()) { + return; + } + + Map allowedSortFilterNames = getEntityColAndTypeBySubmittedNameMap(); + + for (SortBy sortByItem : sortBy) { + if (sortByItem.getSortedOn() == null || sortByItem.getSortedOn().isEmpty()) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "sortedOn attribute not provided in element of sortBy list"); + } + + EntityColumnNameAndType sortColumnEntityNameAndType = getEntityColAndTypeBySubmittedNameMap().get(sortByItem.getSortedOn()); + + if (sortColumnEntityNameAndType == null) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, + String.format("Supplied sortColumn [%s] not available in allowed names [%s]", sortByItem.getSortedOn(), allowedSortFilterNames.keySet()) + ); + } + } + + this.sortBy = sortBy; + } + + @JsonIgnore + public Map getEntityColAndTypeBySubmittedNameMap() throws BrAPIServerException { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, String.format("Sort/Filtering not implemented for %s", this.getClass().getSimpleName())); + } } diff --git a/src/main/java/io/swagger/model/core/SortBy.java b/src/main/java/io/swagger/model/core/SortBy.java deleted file mode 100644 index 8a132a3e..00000000 --- a/src/main/java/io/swagger/model/core/SortBy.java +++ /dev/null @@ -1,56 +0,0 @@ -package io.swagger.model.core; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - -public enum SortBy { - - STUDYDBID("studyDbId"), - - STARTDATE("startDate"), - - ENDDATE("endDate"), - - TRIALDBID("trialDbId"), - - TRIALNAME("trialName"), - - PROGRAMDBID("programDbId"), - - LOCATIONDBID("locationDbId"), - - SEASONDBID("seasonDbId"), - - STUDYTYPE("studyType"), - - STUDYNAME("studyName"), - - STUDYLOCATION("studyLocation"), - - PROGRAMNAME("programName"), - - GERMPLASMDBID("germplasmDbId"), - - OBSERVATIONVARIABLEDBID("observationVariableDbId"); -private String value; - - SortBy(String value) { - this.value = value; - } - - @Override - @JsonValue - public String toString() { - return String.valueOf(value); - } - - @JsonCreator - public static SortBy fromValue(String text) { - for (SortBy b : SortBy.values()) { - if (String.valueOf(b.value).equals(text)) { - return b; - } - } - return null; - } -} diff --git a/src/main/java/io/swagger/model/core/StudySearchRequest.java b/src/main/java/io/swagger/model/core/StudySearchRequest.java index 0b26436b..94e760ff 100644 --- a/src/main/java/io/swagger/model/core/StudySearchRequest.java +++ b/src/main/java/io/swagger/model/core/StudySearchRequest.java @@ -1,14 +1,36 @@ package io.swagger.model.core; +import java.util.Map; import java.util.Objects; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.model.SearchRequest; +import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; +import org.brapi.test.BrAPITestServer.model.dto.EntityType; + import java.util.ArrayList; import java.util.List; public class StudySearchRequest extends SearchRequest { + + // Key - allowed sort or field filter name for this entity + // Value = entity field name and type that represents the submitted field. Used later on in query building. + private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = + Map.ofEntries( + Map.entry("germplasmDbId", new EntityColumnNameAndType("*obsunit.germplasm.id", EntityType.UUID)), + Map.entry("locationDbId", new EntityColumnNameAndType("location.id", EntityType.UUID)), + Map.entry("observationVariableDbId", new EntityColumnNameAndType("*observation.observationVariable.id", EntityType.UUID)), + Map.entry("programDbId", new EntityColumnNameAndType("trial.program.id", EntityType.UUID)), + Map.entry("programName", new EntityColumnNameAndType("trial.program.name", EntityType.TEXT)), + Map.entry("seasonDbId", new EntityColumnNameAndType("*season.id", EntityType.UUID)), + Map.entry("studyDbId", new EntityColumnNameAndType("id", EntityType.UUID)), + Map.entry("studyLocation", new EntityColumnNameAndType("location.id", EntityType.UUID)), + Map.entry("trialDbId", new EntityColumnNameAndType("trial.id", EntityType.UUID)), + Map.entry("studyType", new EntityColumnNameAndType("studyName", EntityType.TEXT)), + Map.entry("studyName", new EntityColumnNameAndType("studyName", EntityType.TEXT)) + ); + @JsonProperty("commonCropNames") private List commonCropNames = null; @@ -54,12 +76,6 @@ public class StudySearchRequest extends SearchRequest { @JsonProperty("seasonDbIds") private List seasonDbIds = null; - @JsonProperty("sortBy") - private SortBy sortBy = null; - - @JsonProperty("sortOrder") - private SortOrder sortOrder = null; - @JsonProperty("studyCodes") private List studyCodes = null; @@ -376,32 +392,6 @@ public void setSeasonDbIds(List seasonDbIds) { this.seasonDbIds = seasonDbIds; } - public StudySearchRequest sortBy(SortBy sortBy) { - this.sortBy = sortBy; - return this; - } - - public SortBy getSortBy() { - return sortBy; - } - - public void setSortBy(SortBy sortBy) { - this.sortBy = sortBy; - } - - public StudySearchRequest sortOrder(SortOrder sortOrder) { - this.sortOrder = sortOrder; - return this; - } - - public SortOrder getSortOrder() { - return sortOrder; - } - - public void setSortOrder(SortOrder sortOrder) { - this.sortOrder = sortOrder; - } - public StudySearchRequest studyCodes(List studyCodes) { this.studyCodes = studyCodes; return this; @@ -492,7 +482,6 @@ public boolean equals(java.lang.Object o) { && Objects.equals(this.active, studySearchRequest.active) && Objects.equals(this.seasonDbIds, studySearchRequest.seasonDbIds) && Objects.equals(this.sortBy, studySearchRequest.sortBy) - && Objects.equals(this.sortOrder, studySearchRequest.sortOrder) && Objects.equals(this.studyCodes, studySearchRequest.studyCodes) && Objects.equals(this.studyPUIs, studySearchRequest.studyPUIs) && Objects.equals(this.studyTypes, studySearchRequest.studyTypes) && super.equals(o); @@ -503,7 +492,7 @@ public int hashCode() { return Objects.hash(commonCropNames, programDbIds, programNames, trialDbIds, trialNames, studyDbIds, studyNames, locationDbIds, locationNames, germplasmDbIds, germplasmNames, observationVariableDbIds, observationVariableNames, externalReferenceIds, externalReferenceSources, active, seasonDbIds, sortBy, - sortOrder, studyCodes, studyPUIs, studyTypes, super.hashCode()); + studyCodes, studyPUIs, studyTypes, super.hashCode()); } @Override @@ -529,7 +518,6 @@ public String toString() { sb.append(" active: ").append(toIndentedString(active)).append("\n"); sb.append(" seasonDbIds: ").append(toIndentedString(seasonDbIds)).append("\n"); sb.append(" sortBy: ").append(toIndentedString(sortBy)).append("\n"); - sb.append(" sortOrder: ").append(toIndentedString(sortOrder)).append("\n"); sb.append(" studyCodes: ").append(toIndentedString(studyCodes)).append("\n"); sb.append(" studyPUIs: ").append(toIndentedString(studyPUIs)).append("\n"); sb.append(" studyTypes: ").append(toIndentedString(studyTypes)).append("\n"); @@ -583,8 +571,6 @@ public Integer getTotalParameterCount() { count += this.seasonDbIds.size(); if (this.sortBy != null) count += 1; - if (this.sortOrder != null) - count += 1; if (this.studyCodes != null) count += this.studyCodes.size(); if (this.studyPUIs != null) @@ -593,4 +579,9 @@ public Integer getTotalParameterCount() { count += this.studyTypes.size(); return count; } + + @Override + public Map getEntityColAndTypeBySubmittedNameMap() { + return ALLOWED_SORT_AND_FILTER_FIELDS; + } } diff --git a/src/main/java/io/swagger/model/core/TrialSearchRequest.java b/src/main/java/io/swagger/model/core/TrialSearchRequest.java index 830dde51..2760e5d5 100644 --- a/src/main/java/io/swagger/model/core/TrialSearchRequest.java +++ b/src/main/java/io/swagger/model/core/TrialSearchRequest.java @@ -1,13 +1,32 @@ package io.swagger.model.core; -import java.util.Objects; +import java.util.*; + import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.model.SearchRequest; -import java.util.ArrayList; -import java.util.List; +import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; +import org.brapi.test.BrAPITestServer.model.dto.EntityType; + import java.time.LocalDate; public class TrialSearchRequest extends SearchRequest { + + // Key - allowed sort or field filter name for this entity + // Value = entity field name and type that represents the submitted field. Used later on in query building. + private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = + Map.of( + "trialName", new EntityColumnNameAndType("trialName", EntityType.TEXT), + "createdDate", new EntityColumnNameAndType("createdDate", EntityType.TEXT), + "createdBy", new EntityColumnNameAndType("createdBy", EntityType.TEXT), + "trialDbId", new EntityColumnNameAndType("id", EntityType.UUID), + "programDbId",new EntityColumnNameAndType("program.id", EntityType.UUID), + "startDate", new EntityColumnNameAndType("startDate", EntityType.TEXT), + "endDate", new EntityColumnNameAndType("endDate", EntityType.TEXT), + "active", new EntityColumnNameAndType("active", EntityType.BOOLEAN), + "programName", new EntityColumnNameAndType("program.name", EntityType.TEXT), + "locationDbId", new EntityColumnNameAndType("*studies.location.id", EntityType.UUID) + ); + @JsonProperty("commonCropNames") private List commonCropNames = null; @@ -50,28 +69,6 @@ public class TrialSearchRequest extends SearchRequest { @JsonProperty("trialPUIs") private List trialPUIs = null; - @JsonProperty("sortBy") - private SortBy sortBy = null; - - @JsonProperty("sortOrder") - private SortOrder sortOrder = null; - - public SortBy getSortBy() { - return sortBy; - } - - public void setSortBy(SortBy sortBy) { - this.sortBy = sortBy; - } - - public SortOrder getSortOrder() { - return sortOrder; - } - - public void setSortOrder(SortOrder sortOrder) { - this.sortOrder = sortOrder; - } - public TrialSearchRequest commonCropNames(List commonCropNames) { this.commonCropNames = commonCropNames; return this; @@ -445,4 +442,9 @@ public Integer getTotalParameterCount() { count += this.trialPUIs.size(); return count; } + + @Override + public Map getEntityColAndTypeBySubmittedNameMap() { + return ALLOWED_SORT_AND_FILTER_FIELDS; + } } diff --git a/src/main/java/io/swagger/model/germ/GermplasmNewRequest.java b/src/main/java/io/swagger/model/germ/GermplasmNewRequest.java index 253783e7..7317f374 100644 --- a/src/main/java/io/swagger/model/germ/GermplasmNewRequest.java +++ b/src/main/java/io/swagger/model/germ/GermplasmNewRequest.java @@ -97,6 +97,12 @@ public class GermplasmNewRequest extends BrAPIDataModel { @JsonProperty("taxonIds") private List taxonIds = null; + @JsonProperty("programDbId") + private String programDbId = null; + + @JsonProperty("programName") + private String programName = null; + public String getBreedingMethodName() { return breedingMethodName; } @@ -510,6 +516,22 @@ public void setTaxonIds(List taxonIds) { this.taxonIds = taxonIds; } + public String getProgramDbId() { + return programDbId; + } + + public void setProgramDbId(String programDbId) { + this.programDbId = programDbId; + } + + public String getProgramName() { + return programName; + } + + public void setProgramName(String programName) { + this.programName = programName; + } + @Override public boolean equals(java.lang.Object o) { if (this == o) { @@ -550,7 +572,9 @@ public boolean equals(java.lang.Object o) { && Objects.equals(this.subtaxa, germplasmNewRequest.subtaxa) && Objects.equals(this.subtaxaAuthority, germplasmNewRequest.subtaxaAuthority) && Objects.equals(this.synonyms, germplasmNewRequest.synonyms) - && Objects.equals(this.taxonIds, germplasmNewRequest.taxonIds); + && Objects.equals(this.taxonIds, germplasmNewRequest.taxonIds) + && Objects.equals(this.programDbId, germplasmNewRequest.programDbId) + && Objects.equals(this.programName, germplasmNewRequest.programName); } @Override @@ -560,7 +584,7 @@ public int hashCode() { countryOfOriginCode, defaultDisplayName, documentationURL, donors, externalReferences, genus, germplasmName, germplasmOrigin, germplasmPUI, germplasmPreprocessing, instituteCode, instituteName, pedigree, seedSource, seedSourceDescription, species, speciesAuthority, storageTypes, subtaxa, - subtaxaAuthority, synonyms, taxonIds); + subtaxaAuthority, synonyms, taxonIds, programDbId, programName); } @Override @@ -600,6 +624,8 @@ public String toString() { sb.append(" subtaxaAuthority: ").append(toIndentedString(subtaxaAuthority)).append("\n"); sb.append(" synonyms: ").append(toIndentedString(synonyms)).append("\n"); sb.append(" taxonIds: ").append(toIndentedString(taxonIds)).append("\n"); + sb.append(" programDbId: ").append(toIndentedString(programDbId)).append("\n"); + sb.append(" programName: ").append(toIndentedString(programName)).append("\n"); sb.append("}"); return sb.toString(); } diff --git a/src/main/java/io/swagger/model/sort/SortBy.java b/src/main/java/io/swagger/model/sort/SortBy.java new file mode 100644 index 00000000..bb286fa6 --- /dev/null +++ b/src/main/java/io/swagger/model/sort/SortBy.java @@ -0,0 +1,30 @@ +package io.swagger.model.sort; + +public class SortBy { + private String sortedOn; + private SortOrder sortOrder = SortOrder.ASC; + + public SortBy(String sortedOn, + SortOrder sortOrder) { + this.sortedOn = sortedOn; + this.sortOrder = sortOrder; + } + + public SortBy() {} + + public String getSortedOn() { + return sortedOn; + } + + public void setSortedOn(String sortedOn) { + this.sortedOn = sortedOn; + } + + public SortOrder getSortOrder() { + return sortOrder; + } + + public void setSortOrder(SortOrder sortOrder) { + this.sortOrder = sortOrder; + } +} diff --git a/src/main/java/io/swagger/model/core/SortOrder.java b/src/main/java/io/swagger/model/sort/SortOrder.java similarity index 92% rename from src/main/java/io/swagger/model/core/SortOrder.java rename to src/main/java/io/swagger/model/sort/SortOrder.java index 0e5b36bd..b8a85ebf 100644 --- a/src/main/java/io/swagger/model/core/SortOrder.java +++ b/src/main/java/io/swagger/model/sort/SortOrder.java @@ -1,4 +1,4 @@ -package io.swagger.model.core; +package io.swagger.model.sort; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; @@ -26,6 +26,6 @@ public static SortOrder fromValue(String text) { return b; } } - return null; + return ASC; } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/BrapiExceptionHandler.java b/src/main/java/org/brapi/test/BrAPITestServer/BrapiExceptionHandler.java index 7ee15a60..fa6508f0 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/BrapiExceptionHandler.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/BrapiExceptionHandler.java @@ -1,13 +1,12 @@ package org.brapi.test.BrAPITestServer; import java.util.ArrayList; +import java.util.Map; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.lang.Nullable; import org.springframework.web.bind.MissingServletRequestParameterException; @@ -70,4 +69,36 @@ private ResponseEntity buildErrorResponse(HttpStatus code, String messag return new ResponseEntity(apiError, code); } + + // This override handles JSON parsing failures detected by Jackson. + // It allows for capture of BrAPI generated BAD Request exceptions that are occur during serialization, + // like those involved in filtering and sorting in search requests. + @Override + protected ResponseEntity handleHttpMessageNotReadable( + HttpMessageNotReadableException ex, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request + ) { + Throwable root = ex.getMostSpecificCause(); + + if (root instanceof BrAPIServerException brapiServerException) { + + ProblemDetail detail = + ProblemDetail.forStatus(HttpStatus.BAD_REQUEST); + + detail.setTitle("Bad Request"); + detail.setDetail(brapiServerException.getResponseMessage()); + + return ResponseEntity.badRequest().body(detail); + } + + // Delegate back to Spring default behavior + return super.handleHttpMessageNotReadable( + ex, + headers, + status, + request + ); + } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/controller/germ/GermplasmApiController.java b/src/main/java/org/brapi/test/BrAPITestServer/controller/germ/GermplasmApiController.java index 52e58a1d..dd462c4b 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/controller/germ/GermplasmApiController.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/controller/germ/GermplasmApiController.java @@ -156,6 +156,7 @@ public ResponseEntity germplasmGet( @RequestParam(value = "progenyDbId", required = false) String progenyDbId, @RequestParam(value = "commonCropName", required = false) String commonCropName, @RequestParam(value = "programDbId", required = false) String programDbId, + @RequestParam(value = "programName", required = false) String programName, @RequestParam(value = "externalReferenceID", required = false) String externalReferenceID, @RequestParam(value = "externalReferenceId", required = false) String externalReferenceId, @RequestParam(value = "externalReferenceSource", required = false) String externalReferenceSource, @@ -170,7 +171,7 @@ public ResponseEntity germplasmGet( Metadata metadata = generateMetaDataTemplate(page, pageSize); List data = germplasmService.findGermplasm(germplasmPUI, germplasmDbId, germplasmName, accessionNumber, collection, binomialName, genus, species, trialDbId, studyDbId, synonym, parentDbId, - progenyDbId, commonCropName, programDbId, externalReferenceId, externalReferenceID, + progenyDbId, commonCropName, programDbId, programName, externalReferenceId, externalReferenceID, externalReferenceSource, metadata); return responseOK(new GermplasmListResponse(), new GermplasmListResponseResult(), data, metadata); } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityColumnNameAndType.java b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityColumnNameAndType.java new file mode 100644 index 00000000..71f4a9f2 --- /dev/null +++ b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityColumnNameAndType.java @@ -0,0 +1,28 @@ +package org.brapi.test.BrAPITestServer.model.dto; + +/** + * This class is used to map an entity's column name to the SQL type of lookup that should be completed for filter searches. + * The entityColumnName is also used for sorts as well. + */ +public class EntityColumnNameAndType { + String entityColumnName; + EntityType entityType; + + public EntityColumnNameAndType(String entityColumnName, EntityType entityType) { + this.entityColumnName = entityColumnName; + this.entityType = entityType; + } + + public String getEntityColumnName() { + return entityColumnName; + } + + public EntityType getEntityType() { + return entityType; + } + + @Override + public String toString() { + return this.entityColumnName; + } +} diff --git a/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityType.java b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityType.java new file mode 100644 index 00000000..e69aa988 --- /dev/null +++ b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityType.java @@ -0,0 +1,8 @@ +package org.brapi.test.BrAPITestServer.model.dto; + +public enum EntityType { + TEXT, + UUID, + BOOLEAN + // Add any other entity data types we should filter different in SearchQueryBuilder here +} diff --git a/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/TrialEntity.java b/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/TrialEntity.java index 1c9be57a..5f9bb989 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/TrialEntity.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/TrialEntity.java @@ -4,8 +4,10 @@ import org.brapi.test.BrAPITestServer.model.entity.BrAPIPrimaryEntity; import org.brapi.test.BrAPITestServer.model.entity.pheno.ObservationEntity; import org.brapi.test.BrAPITestServer.model.entity.pheno.ObservationUnitEntity; +import org.hibernate.annotations.Formula; import org.hibernate.annotations.Where; +import java.time.OffsetDateTime; import java.util.Date; import java.util.List; @Entity @@ -38,6 +40,12 @@ public class TrialEntity extends BrAPIPrimaryEntity { @Column(name = "soft_deleted") private boolean softDeleted; + @Formula("(additional_info #>> '{createdDate}')") + private String createdDate; + + @Formula("(additional_info #>> '{createdBy,userName}')") + private String createdBy; + @ManyToOne(fetch = FetchType.LAZY) private CropEntity crop; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/org/brapi/test/BrAPITestServer/model/entity/germ/GermplasmEntity.java b/src/main/java/org/brapi/test/BrAPITestServer/model/entity/germ/GermplasmEntity.java index 1c5192db..8b464b85 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/model/entity/germ/GermplasmEntity.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/model/entity/germ/GermplasmEntity.java @@ -9,6 +9,7 @@ import org.brapi.test.BrAPITestServer.model.entity.BrAPIPrimaryEntity; import org.brapi.test.BrAPITestServer.model.entity.SearchRequestEntity; import org.brapi.test.BrAPITestServer.model.entity.core.CropEntity; +import org.brapi.test.BrAPITestServer.model.entity.core.ProgramEntity; import org.brapi.test.BrAPITestServer.model.entity.germ.GermplasmInstituteEntity.InstituteTypeEnum; import org.brapi.test.BrAPITestServer.model.entity.pheno.ObservationUnitEntity; import org.brapi.test.BrAPITestServer.model.entity.pheno.TaxonEntity; @@ -96,6 +97,8 @@ public class GermplasmEntity extends BrAPIPrimaryEntity { private List typeOfGermplasmStorageCode; @Column(name = "soft_deleted") private boolean softDeleted; + @ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY) + private ProgramEntity program; public GermplasmInstituteEntity getHostInstitute() { if (getInstitutes() != null) { @@ -368,4 +371,12 @@ public void setTypeOfGermplasmStorageCode(List typeOf public void setSoftDeleted(boolean sofDeleted) { this.softDeleted = sofDeleted; } + public ProgramEntity getProgram() { + return program; + } + + public void setProgram(ProgramEntity program) { + this.program = program; + } + } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index 80bf5bc5..d18a30a0 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -6,8 +6,13 @@ import java.time.LocalDate; import java.time.OffsetDateTime; +import io.swagger.model.FilterBy; import io.swagger.model.GeoJSONSearchArea; -import io.swagger.model.core.SortOrder; +import io.swagger.model.sort.SortBy; +import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; +import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; +import org.brapi.test.BrAPITestServer.model.dto.EntityType; +import org.springframework.http.HttpStatus; public class SearchQueryBuilder { @@ -17,6 +22,8 @@ public class SearchQueryBuilder { private String defaultSort; private String sortClause; private Map params; + private List joinedTables = new ArrayList<>(); + private List joinedFetchedTables = new ArrayList<>(); private Class clazz; public SearchQueryBuilder(Class clazz) { @@ -133,6 +140,26 @@ public SearchQueryBuilder appendSingle(UUID single, String columnName) { return this; } + public SearchQueryBuilder appendLike(String like, String columnName) { + String paramName = paramFilterPattern(columnName); + + if (like != null) { + this.whereClause += "AND lower(" + entityPrefix(columnName) + ") LIKE :" + paramName + " "; + this.params.put(paramName, "%" + like + "%"); + } + return this; + } + + public SearchQueryBuilder appendLikeIDs(String like, String columnName) { + String paramName = paramFilterPattern(columnName); + + if (like != null) { + this.whereClause += "AND cast(" + entityPrefix(columnName) + " as String) LIKE :" + paramName + " "; + this.params.put(paramName, "%" + like + "%"); + } + return this; + } + public > SearchQueryBuilder appendEnum(E enumVal, String columnName) { String paramName = paramFilter(columnName); if (enumVal != null) { @@ -258,13 +285,31 @@ public SearchQueryBuilder withExRefs(List exRefIds, List exRe } public SearchQueryBuilder join(String join, String name) { - this.selectClause += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; - this.selectOnlyIds += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; + + if (!this.joinedTables.contains(join) && !this.joinedFetchedTables.contains(join)) { + this.selectClause += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; + this.selectOnlyIds += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; + this.joinedTables.add(join); + } return this; } public SearchQueryBuilder leftJoinFetch(String join, String name) { - this.selectClause += generateLeftJoinFetch(join, name); + return leftJoinFetch(join, name, false); + } + + public SearchQueryBuilder leftJoinFetch(String join, String name, boolean overrideExistingJoin) { + + if (this.joinedTables.contains(join) && overrideExistingJoin) { + // Override existing normal join with join fetch if it already exists in a query + // This override does not change the alias to the name provided, and assumes further usages will use the same alias. + this.selectClause = this.selectClause.replace("JOIN " + entityPrefix(join), "LEFT JOIN FETCH " + entityPrefix(join)); + this.joinedFetchedTables.add(join); + this.joinedTables.remove(join); + } else if (!this.joinedFetchedTables.contains(join)) { + this.selectClause += generateLeftJoinFetch(join, name); + this.joinedFetchedTables.add(join); + } return this; } @@ -280,6 +325,9 @@ public SearchQueryBuilder removeAndReplaceLeftJoinFetch(String join, this.selectClause = this.selectClause.replace(generateLeftJoinFetch(existingJoin, existingName), generateLeftJoinFetch(join, name)); + this.joinedFetchedTables.remove(existingJoin); + this.joinedFetchedTables.add(join); + return this; } @@ -290,6 +338,7 @@ public SearchQueryBuilder removeAndReplaceLeftJoinFetch(String join, public SearchQueryBuilder removeLeftJoinFetch(String join, String name) { this.selectClause = this.selectClause.replace(generateLeftJoinFetch(join, name), ""); + this.joinedFetchedTables.remove(join); return this; } @@ -311,14 +360,108 @@ private String paramFilter(String param) { return param.replace('.', '_').replace('*', '_'); } - public SearchQueryBuilder withSort(String sortByStr, SortOrder sortOrder) { - String sortOrderStr = "ASC"; - if (sortOrder != null) { - sortOrderStr = sortOrder.toString(); + private String paramFilterPattern(String param) { + if (param == null) + return ""; + return param.replace('.', '_').replace('*', '_') + "Pattern"; + } + + /** + * Takes a list of SortBy options that should typically come in a searchRequest, along with a map of the validated + * columns names. + * Applies the entries in the list to sort the SearchQuery. + * + * A SortBy has + * - A column name + * - An order (DESC, ASC) + */ + public SearchQueryBuilder sortBy(List sortBy, Map entityColAndTypeBySubmittedName) throws BrAPIServerException { + + if (sortBy == null || sortBy.isEmpty()) { + return this; } - this.sortClause += " ORDER BY " + entityPrefix(sortByStr) + " " + sortOrderStr; + for (SortBy sort : sortBy) { + // At this point, the submitted sortBy name has been verified to be in entityColAndTypeBySubmittedName + EntityColumnNameAndType entityColumnNameAndType = entityColAndTypeBySubmittedName.get(sort.getSortedOn()); + + String entityColName = entityColumnNameAndType.getEntityColumnName(); + + if (entityColName.startsWith("*")) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "Sorting on one to many relationships not supported"); + } + + String[] split = entityColName.split("\\."); + + if (split.length > 2) { + // TODO: Implement this if it becomes a requirement + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "Sorting on a table greater than one level from primary entity not allowed"); + } + + if (split.length == 2 && !split[1].equals("id")) { + leftJoinFetch(split[0], split[0], true); + } + + sort.setSortedOn(entityColName); + + if (sortBy.getFirst().equals(sort)) { + this.sortClause += " ORDER BY "; + buildSort(sort); + } else { + this.sortClause += ", "; + buildSort(sort); + } + } return this; } + + private void buildSort(SortBy sort) { + this.sortClause += entityPrefix(sort.getSortedOn()) + " " + sort.getSortOrder() + " "; + } + + /** + * Takes a list of FilterBy options that should typically come in a searchRequest, along with a map of the validated + * columns names and the data type they represent for accurate filtering on different data types. + * Applies the entries in the list to filter the SearchQuery. + * + * A FilterBy has + * - A column name + * - A value which the column name should be filtered on + */ + public SearchQueryBuilder filterBy(List filterBy, Map entityColAndTypeBySubmittedName) throws BrAPIServerException { + SearchQueryBuilder searchQuery = this; + + if (filterBy == null || filterBy.isEmpty()) { + return searchQuery; + } + + for (FilterBy filter : filterBy) { + // At this point, the submitted filterBy column name has been verified to be in entityColAndTypeBySubmittedName + EntityColumnNameAndType entityColumnNameAndType = entityColAndTypeBySubmittedName.get(filter.getFilterOn()); + + if (entityColumnNameAndType.getEntityColumnName().startsWith("*")) { + joinCollectionColumn(entityColumnNameAndType.getEntityColumnName()); + } + + if (entityColumnNameAndType.getEntityType() == EntityType.TEXT) { + searchQuery = appendLike(filter.getValue().toLowerCase(), entityColumnNameAndType.getEntityColumnName()); + } else if (entityColumnNameAndType.getEntityType() == EntityType.UUID) { + searchQuery = appendLikeIDs(filter.getValue(), entityColumnNameAndType.getEntityColumnName()); + } + } + + return searchQuery; + } + + /** + * This helper method joins the table that a collection column name is related to if it doesn't exist already. + * This is particularly important for filter search requests because if the join doesn't exist, and it is referenced + * the query will not execute. + */ + private void joinCollectionColumn(String submittedSortFilterColumnName) { + String joinTableName = submittedSortFilterColumnName.substring(1, submittedSortFilterColumnName.indexOf(".")); + + this.join(joinTableName, joinTableName); + } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ProgramService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ProgramService.java index 720f731b..9f11a8f7 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ProgramService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ProgramService.java @@ -1,9 +1,7 @@ package org.brapi.test.BrAPITestServer.service.core; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.util.*; +import java.util.stream.Collectors; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerDbIdNotFoundException; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; @@ -76,14 +74,33 @@ public List findPrograms(ProgramSearchRequest request, Metadata metadat return programs; } - public List findByIds(List programDbIds) { - var result = new ArrayList(); + public List findByIds(List programDbIds) throws BrAPIServerException { + List result = new ArrayList<>(); if (programDbIds.isEmpty()) { return result; } - return programRepository.findByIdIn(programDbIds.stream().map(UUID::fromString).toList()); + // Dedup programIds by loading into set + Set programDbIdSet = new HashSet<>(programDbIds); + + result = programRepository.findByIdIn(programDbIds.stream().map(UUID::fromString).toList()); + + if (programDbIdSet.size() != result.size()) { + List dbIdsNotFound = new ArrayList<>(); + + Set foundDbIds = result.stream().map(pe -> pe.getId().toString()).collect(Collectors.toSet()); + + programDbIdSet.forEach(dbId -> { + if (!foundDbIds.contains(dbId)) { + dbIdsNotFound.add(dbId); + } + }); + + throw new BrAPIServerException(HttpStatus.NOT_FOUND, String.format("The following submitted programDbIds were not found in the db: [%s]", dbIdsNotFound)); + } + + return result; } public Program getProgram(String programDbId) throws BrAPIServerException { diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java index 1b6526ea..ae6a297b 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java @@ -3,10 +3,10 @@ import java.util.*; import java.util.stream.Collectors; +import io.swagger.model.sort.SortBy; import org.apache.commons.lang3.StringUtils; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerDbIdNotFoundException; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; -import org.brapi.test.BrAPITestServer.model.entity.BrAPIBaseEntity; import org.brapi.test.BrAPITestServer.model.entity.core.CropEntity; import org.brapi.test.BrAPITestServer.model.entity.core.DataLinkEntity; import org.brapi.test.BrAPITestServer.model.entity.core.EnvironmentParametersEntity; @@ -38,8 +38,7 @@ import io.swagger.model.Metadata; import io.swagger.model.core.Contact; import io.swagger.model.core.EnvironmentParameter; -import io.swagger.model.core.SortBy; -import io.swagger.model.core.SortOrder; +import io.swagger.model.sort.SortOrder; import io.swagger.model.core.Study; import io.swagger.model.core.StudyExperimentalDesign; import io.swagger.model.core.StudyGrowthFacility; @@ -108,10 +107,11 @@ public List findStudies(String commonCropName, String studyType, String p request.addObservationVariableDbIdsItem(observationVariableDbId); if (active != null) request.setActive(active); - if (sortBy != null && SortBy.fromValue(sortBy) != null) - request.setSortBy(SortBy.fromValue(sortBy)); - if (sortOrder != null && SortOrder.fromValue(sortOrder) != null) - request.setSortOrder(SortOrder.fromValue(sortOrder)); + if (sortBy != null) { + SortBy sortByElement = new SortBy(sortBy, SortOrder.fromValue(sortOrder)); + + request.setSortBy(List.of(sortByElement)); + } request.addExternalReferenceItem(externalReferenceId, externalReferenceID, externalReferenceSource); @@ -159,7 +159,7 @@ public List findStudies(StudySearchRequest request, Metadata metaData) .appendList(request.getStudyDbIds(), "id").appendList(request.getStudyNames(), "studyName") .appendList(request.getStudyPUIs(), "studyPUI").appendList(request.getStudyTypes(), "studyType") .appendList(request.getTrialDbIds(), "trial.id").appendList(request.getTrialNames(), "trial.trialName") - .withSort(getSortByField(request.getSortBy()), request.getSortOrder()); + .sortBy(request.getSortByElements(), request.getEntityColAndTypeBySubmittedNameMap()); Page studiesPage = studyRepository.findAllBySearchAndPaginate(searchQuery, pageReq); PagingUtility.calculateMetaData(metaData, studiesPage); @@ -566,47 +566,4 @@ private EnvironmentParametersEntity convertToEntity(EnvironmentParameter param) return entity; } - private String getSortByField(SortBy sortBy) { - String sortByStr = "id"; - if (sortBy != null) { - switch (sortBy) { - case GERMPLASMDBID: - sortByStr = "*obsunit.germplasm.id"; - break; - case LOCATIONDBID: - sortByStr = "location.id"; - break; - case OBSERVATIONVARIABLEDBID: - sortByStr = "*observation.observationVariable.id"; - break; - case PROGRAMDBID: - sortByStr = "trial.program.id"; - break; - case PROGRAMNAME: - sortByStr = "trial.program.name"; - break; - case SEASONDBID: - sortByStr = "*season.id"; - break; - case STUDYDBID: - sortByStr = "id"; - break; - case STUDYLOCATION: - sortByStr = "location.id"; - break; - case TRIALDBID: - sortByStr = "trial.id "; - break; - case STUDYTYPE: - sortByStr = "studyName"; - break; - case STUDYNAME: - default: - sortByStr = "studyName"; - break; - } - } - return sortByStr; - } - } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java index fb798a5b..21c30c65 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java @@ -4,6 +4,8 @@ import java.util.stream.Collectors; import io.swagger.model.core.*; +import io.swagger.model.sort.SortBy; +import io.swagger.model.sort.SortOrder; import jakarta.validation.Valid; import org.brapi.test.BrAPITestServer.exceptions.BatchDeleteWrongTypeException; @@ -94,11 +96,10 @@ public List findTrials(@Valid String commonCropName, @Valid String contac request.setSearchDateRangeStart(searchDateRangeStart); if (searchDateRangeEnd != null) request.setSearchDateRangeEnd(searchDateRangeEnd); - if (sortBy != null && SortBy.fromValue(sortBy) != null) - request.setSortBy(SortBy.fromValue(sortBy)); - if (sortOrder != null && SortOrder.fromValue(sortOrder) != null) - request.setSortOrder(SortOrder.fromValue(sortOrder)); - + if (sortBy != null) { + SortBy querySortBy = new SortBy(sortBy, SortOrder.fromValue(sortOrder)); + request.setSortBy(List.of(querySortBy)); + } request.addExternalReferenceItem(externalReferenceId, externalReferenceID, externalReferenceSource); return findTrials(request, metadata); } @@ -113,20 +114,21 @@ public List findTrials(@Valid TrialSearchRequest request, Metadata metada searchQuery = searchQuery.join("contacts", "contact"); } if (request.getStudyDbIds() != null || request.getStudyNames() != null) { - searchQuery = searchQuery.join("studies", "study"); + searchQuery = searchQuery.join("studies", "studies"); } searchQuery = searchQuery.withExRefs(request.getExternalReferenceIDs(), request.getExternalReferenceSources()) .appendList(request.getCommonCropNames(), "crop.cropName") .appendList(request.getContactDbIds(), "*contact.id") - .appendList(request.getLocationDbIds(), "*study.location.id") - .appendList(request.getLocationNames(), "*study.location.locationName") + .appendList(request.getLocationDbIds(), "*studies.location.id") + .appendList(request.getLocationNames(), "*studies.location.locationName") .appendList(request.getProgramDbIds(), "program.id") - .appendList(request.getProgramNames(), "program.name").appendList(request.getStudyDbIds(), "*study.id") - .appendList(request.getStudyNames(), "*study.studyName").appendList(request.getTrialDbIds(), "id") + .appendList(request.getProgramNames(), "program.name").appendList(request.getStudyDbIds(), "*studies.id") + .appendList(request.getStudyNames(), "*studies.studyName").appendList(request.getTrialDbIds(), "id") .appendList(request.getTrialNames(), "trialName") .appendDateRange(request.getSearchDateRangeStart(), request.getSearchDateRangeEnd(), "startDate") - .withSort(getSortByField(request.getSortBy()), request.getSortOrder()); + .sortBy(request.getSortByElements(), request.getEntityColAndTypeBySubmittedNameMap()) + .filterBy(request.getFilterBy(), request.getEntityColAndTypeBySubmittedNameMap()); Page trialsPage = trialRepository.findAllBySearchAndPaginate(searchQuery, pageReq); PagingUtility.calculateMetaData(metadata, trialsPage); @@ -356,34 +358,4 @@ private PublicationEntity convertToEntity(TrialNewRequestPublications pub) { return entity; } - - private String getSortByField(SortBy sortBy) { - String sortByStr = "id"; - if (sortBy != null) { - switch (sortBy) { - case STARTDATE: - sortByStr = "startDate"; - break; - case ENDDATE: - sortByStr = "endDate"; - break; - case TRIALNAME: - sortByStr = "trialName"; - break; - case PROGRAMDBID: - sortByStr = "program.id"; - break; - case PROGRAMNAME: - sortByStr = "program.name"; - break; - case TRIALDBID: - default: - sortByStr = "id"; - break; - } - } - - return sortByStr; - } - } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java index 2946fd58..5d3e4689 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java @@ -4,6 +4,7 @@ import java.util.*; import java.util.stream.Collectors; +import io.swagger.model.core.ProgramSearchRequest; import io.swagger.model.germ.*; import jakarta.validation.Valid; @@ -12,6 +13,7 @@ import org.brapi.test.BrAPITestServer.model.entity.BrAPIBaseEntity; import org.brapi.test.BrAPITestServer.model.entity.ExternalReferenceEntity; import org.brapi.test.BrAPITestServer.model.entity.core.CropEntity; +import org.brapi.test.BrAPITestServer.model.entity.core.ProgramEntity; import org.brapi.test.BrAPITestServer.model.entity.germ.*; import org.brapi.test.BrAPITestServer.model.entity.germ.GermplasmInstituteEntity.InstituteTypeEnum; import org.brapi.test.BrAPITestServer.model.entity.pheno.TaxonEntity; @@ -23,6 +25,7 @@ import org.brapi.test.BrAPITestServer.service.SearchQueryBuilder; import org.brapi.test.BrAPITestServer.service.UpdateUtility; import org.brapi.test.BrAPITestServer.service.core.CropService; +import org.brapi.test.BrAPITestServer.service.core.ProgramService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -44,21 +47,23 @@ public class GermplasmService { private final GermplasmDonorRepository donorRepository; private final BreedingMethodService breedingMethodService; private final CropService cropService; + private final ProgramService programService; @Autowired public GermplasmService(GermplasmRepository germplasmRepository, GermplasmDonorRepository donorRepository, - BreedingMethodService breedingMethodService, CropService cropService) { + BreedingMethodService breedingMethodService, CropService cropService, ProgramService programService) { this.germplasmRepository = germplasmRepository; this.donorRepository = donorRepository; this.breedingMethodService = breedingMethodService; this.cropService = cropService; + this.programService = programService; } public List findGermplasm(String germplasmPUI, String germplasmDbId, String germplasmName, String accessionNumber, String collection, String binomialName, String genus, String species, String trialDbId, String studyDbId, String synonym, String parentDbId, String progenyDbId, - String commonCropName, String programDbId, String externalReferenceId, String externalReferenceID, + String commonCropName, String programDbId, String programName, String externalReferenceId, String externalReferenceID, String externalReferenceSource, Metadata metadata) throws BrAPIServerException { @@ -93,6 +98,8 @@ public List findGermplasm(String germplasmPUI, String germplasmDbId, request.addCommonCropNamesItem(commonCropName); if (programDbId != null) request.addProgramDbIdsItem(programDbId); + if (programName != null) + request.addProgramNamesItem(programName); request.addExternalReferenceItem(externalReferenceId, externalReferenceID, externalReferenceSource); @@ -151,6 +158,7 @@ public Page findGermplasmEntities(@Valid GermplasmSearchRequest return germs; } + // TODO: Investigate/consider removing this code. It is largely unused by DeltaBreed now, as paginating is the least memory exhaustive option. [BI-3020] public List findGermplasmEntitiesWithoutPaging(@Valid GermplasmSearchRequest request) { SearchQueryBuilder searchQuery = buildGermplasmSearchQuery(request); @@ -171,12 +179,10 @@ private SearchQueryBuilder buildGermplasmSearchQuery(GermplasmS .leftJoinFetch("pedigree", "pedigree") .leftJoinFetch("*pedigree.crossingProject", "crossingProject"); - if (request.getProgramDbIds() != null || request.getProgramNames() != null || request.getTrialDbIds() != null + if (request.getTrialDbIds() != null || request.getTrialNames() != null || request.getStudyDbIds() != null || request.getStudyNames() != null) { searchQuery = searchQuery.join("observationUnits", "obsunit") - .appendList(request.getProgramDbIds(), "*obsunit.program.id") - .appendList(request.getProgramNames(), "*obsunit.program.name") .appendList(request.getTrialDbIds(), "*obsunit.trial.id") .appendList(request.getTrialNames(), "*obsunit.trial.name") .appendList(request.getStudyDbIds(), "*obsunit.study.id") @@ -197,6 +203,7 @@ private SearchQueryBuilder buildGermplasmSearchQuery(GermplasmS .appendList(request.getGermplasmNames(), "germplasmName") .appendList(request.getGermplasmPUIs(), "germplasmPUI") .appendList(request.getParentDbIds(), "pedigree.parent1.germplasm.id") + .appendList(request.getProgramDbIds(), "program.id").appendList(request.getProgramNames(), "program.name") // .appendList(request.getProgenyDbIds(), "*progeny.germplasmDbId") .appendList(request.getGenus(), "genus").appendList(request.getSpecies(), "species") .appendNamesList(request.getBinomialNames(), "genus", "genus", "species") @@ -528,6 +535,12 @@ private Germplasm convertFromEntity(GermplasmEntity entity) { germ.setCollection(entity.getCollection()); if (entity.getCrop() != null) germ.setCommonCropName(entity.getCrop().getCropName()); + + if (entity.getProgram() != null) { + germ.setProgramDbId(entity.getProgram().getId().toString()); + germ.setProgramName(entity.getProgram().getName()); + } + germ.setCountryOfOriginCode(entity.getCountryOfOriginCode()); germ.setDefaultDisplayName(entity.getDefaultDisplayName()); germ.setDocumentationURL(entity.getDocumentationURL()); @@ -581,6 +594,11 @@ private List createEntitiesInBatch(List bo .filter(Objects::nonNull) .collect(Collectors.toSet()); + Set programDbIds = body.stream() + .map(GermplasmNewRequest::getProgramDbId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + Map foundBreedingMethodsById = breedingMethodService.findBreedingMethodsByIds(breedingMethodIds) .stream() @@ -590,6 +608,11 @@ private List createEntitiesInBatch(List bo .stream() .collect(Collectors.toMap(CropEntity::getCropName, e -> e)); + Map foundProgramsByDbId + = programService.findByIds(new ArrayList<>(programDbIds)) + .stream() + .collect(Collectors.toMap(ProgramEntity::getId, e -> e)); + for (GermplasmNewRequest request : body) { GermplasmEntity entity = new GermplasmEntity(); @@ -604,7 +627,7 @@ private List createEntitiesInBatch(List bo if (request.getBiologicalStatusOfAccessionCode() != null) entity.setBiologicalStatusOfAccessionCode(request.getBiologicalStatusOfAccessionCode()); if (request.getBreedingMethodDbId() != null) { - entity.setBreedingMethod(foundBreedingMethodsById.get(request.getBreedingMethodDbId())); + entity.setBreedingMethod(foundBreedingMethodsById.get(UUID.fromString(request.getBreedingMethodDbId()))); } if (request.getCollection() != null) entity.setCollection(request.getCollection()); @@ -661,6 +684,10 @@ private List createEntitiesInBatch(List bo updateSynonymEntities(request.getSynonyms(), entity); if (request.getTaxonIds() != null) updateTaxonEntities(request.getTaxonIds(), entity); + if (request.getProgramDbId() != null) { + ProgramEntity program = foundProgramsByDbId.get(UUID.fromString(request.getProgramDbId())); + entity.setProgram(program); + } toSave.add(entity); } @@ -738,6 +765,10 @@ private void updateEntity(GermplasmEntity entity, GermplasmNewRequest request) t updateSynonymEntities(request.getSynonyms(), entity); if (request.getTaxonIds() != null) updateTaxonEntities(request.getTaxonIds(), entity); + if (request.getProgramDbId() != null) { + ProgramEntity program = programService.getProgramEntity(request.getProgramDbId()); + entity.setProgram(program); + } } private void updateTaxonEntities(List taxonIds, GermplasmEntity entity) { diff --git a/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql b/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql new file mode 100644 index 00000000..2166b229 --- /dev/null +++ b/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql @@ -0,0 +1,65 @@ +WITH + levels_and_parent_ou_bi_id AS ( +-- Find observation_unit_level codes that contain a key by checking for a space in the level code. +-- Only other level codes that can exist are block/rep. +-- Then extract the uuid from the level code, since program key is also contained there. + SELECT + id AS level_id, + substring(level_code FROM '^([^ ]+)') AS parent_bi_ou_id + FROM observation_unit_level + WHERE level_code LIKE '% %' + ), + -- This query should pick up external references with a unique external_reference_id and external_reference_source. + -- This is critical for this update as if there are any external references used that are not unique, + -- we could assign the wrong observation_unit_id. + exrefid_source_with_one_ou_connected AS ( + SELECT ex.external_reference_id, ex.external_reference_source, (array_agg(id))[1] AS exref_pk +FROM external_reference ex + JOIN observation_unit_external_references ouex ON ex.id = ouex.external_references_id +WHERE ex.external_reference_source = 'breedinginsight.org/observationunits' +GROUP BY ex.external_reference_id, ex.external_reference_source +HAVING count(*) = 1 + ), + ou_ids_matched_on_levels AS ( +-- Now match the bi-generated exref ou ids to ex refs ids, and keep observation_unit_level ids for matching in next part +SELECT + ou.id AS ou_id, + levels_and_parent_ou_bi_id.level_id +FROM observation_unit ou + JOIN observation_unit_external_references ouex ON ou.id = ouex.observation_unit_entity_id + JOIN exrefid_source_with_one_ou_connected ON exrefid_source_with_one_ou_connected.exref_pk = ouex.external_references_id + JOIN levels_and_parent_ou_bi_id ON exrefid_source_with_one_ou_connected.external_reference_id = levels_and_parent_ou_bi_id.parent_bi_ou_id + ) +UPDATE observation_unit_level +SET level_code = regexp_replace( + observation_unit_level.level_code, + '^[^ ]+', + mol.ou_id::text +) +FROM ou_ids_matched_on_levels mol +WHERE observation_unit_level.id = mol.level_id; + +-- Assertion to assure all expected top-level observation unit db ids were changed from external reference ids to observation unit db ids +DO $$ +DECLARE + top_level_observation_unit_level_count integer; + top_level_ouls_matched_to_ou_id integer; +BEGIN + SELECT COUNT(*) + INTO top_level_observation_unit_level_count + FROM observation_unit_level + WHERE level_code like '% %'; + + SELECT COUNT(*) + INTO top_level_ouls_matched_to_ou_id + FROM observation_unit_level oul + JOIN observation_unit ou on ou.id::text = substring(level_code FROM '^([^ ]+)') + WHERE level_code like '% %'; + + IF top_level_observation_unit_level_count <> top_level_ouls_matched_to_ou_id THEN + RAISE EXCEPTION + 'V006.001 After migration, expected all % observation_unit_level.code rows to match to observation_unit.id, but only % matched', + top_level_observation_unit_level_count, + top_level_ouls_matched_to_ou_id; + END IF; +END $$; \ No newline at end of file diff --git a/src/main/resources/db/migration/V006_002__migrate_sample_ou_ids.sql b/src/main/resources/db/migration/V006_002__migrate_sample_ou_ids.sql new file mode 100644 index 00000000..5ff206d3 --- /dev/null +++ b/src/main/resources/db/migration/V006_002__migrate_sample_ou_ids.sql @@ -0,0 +1,61 @@ +WITH + samples_and_bi_ou_ids AS ( +-- Look for samples that have obsUnitIDs and keep ids in hand for next query + SELECT + id as sample_id, + additional_info ->> 'obsUnitID' AS bi_ou_id + FROM sample + WHERE additional_info ? 'obsUnitID' + ), + exrefid_source_with_one_ou_connected AS ( + SELECT ex.external_reference_id, ex.external_reference_source, (array_agg(id))[1] AS exref_pk + FROM external_reference ex + JOIN observation_unit_external_references ouex ON ex.id = ouex.external_references_id + WHERE ex.external_reference_source = 'breedinginsight.org/observationunits' + GROUP BY ex.external_reference_id, ex.external_reference_source + HAVING COUNT(*) = 1 + ), + ou_ids_matched_on_samples AS ( +-- Now match the bi-generated exref ou ids to ex refs ids, and keep samples ids for matching in update + SELECT + ou.id AS ou_id, + samples_and_bi_ou_ids.sample_id + FROM observation_unit ou + JOIN observation_unit_external_references ouex ON ou.id = ouex.observation_unit_entity_id + JOIN exrefid_source_with_one_ou_connected on exrefid_source_with_one_ou_connected.exref_pk = ouex.external_references_id + JOIN samples_and_bi_ou_ids ON exrefid_source_with_one_ou_connected.external_reference_id = samples_and_bi_ou_ids.bi_ou_id + ) +UPDATE sample +SET additional_info = jsonb_set( + additional_info, + '{obsUnitID}', + to_jsonb(mos.ou_id) +) +FROM ou_ids_matched_on_samples mos +WHERE id = mos.sample_id; + +-- Assertion to ensure all samples with ou in additional info now relate to observation unit db id +DO $$ + DECLARE + samples_with_ou_count integer; + samples_with_ous_matched_to_ou_count integer; + BEGIN + SELECT COUNT(*) + INTO samples_with_ou_count + FROM sample + WHERE additional_info ? 'obsUnitID'; + + SELECT COUNT(*) + INTO samples_with_ous_matched_to_ou_count + FROM sample s + JOIN observation_unit ou on ou.id::text = s.additional_info ->> 'obsUnitID' + WHERE s.additional_info ? 'obsUnitID'; + + IF samples_with_ou_count <> samples_with_ous_matched_to_ou_count THEN + RAISE EXCEPTION + 'V006.002 After migration, expected all % sample.additional_info->>obsUnitID rows to match to observation_unit.id, but only % matched', + samples_with_ou_count, + samples_with_ous_matched_to_ou_count; + END IF; + END +$$; \ No newline at end of file diff --git a/src/main/resources/db/migration/V006_003__add_program_id_to_germplasm_and_migrate.sql b/src/main/resources/db/migration/V006_003__add_program_id_to_germplasm_and_migrate.sql new file mode 100644 index 00000000..eb13b87a --- /dev/null +++ b/src/main/resources/db/migration/V006_003__add_program_id_to_germplasm_and_migrate.sql @@ -0,0 +1,40 @@ +ALTER TABLE germplasm +ADD COLUMN program_id UUID; + +ALTER TABLE germplasm +ADD CONSTRAINT germplasm_program_fk + FOREIGN KEY (program_id) + REFERENCES public.program(id); + +CREATE INDEX germplasm_program_idx ON germplasm (program_id, id); + +UPDATE germplasm set program_id = pquery.program_id +FROM ( + SELECT g.id AS germplasm_id, p.id AS program_id + FROM germplasm g + JOIN germplasm_external_references gex ON g.id = gex.germplasm_entity_id + JOIN external_reference ex ON ex.id = gex.external_references_id + JOIN external_reference ex2 ON ex2.external_reference_id = ex.external_reference_id + JOIN program_external_references pex ON pex.external_references_id = ex2.id + JOIN program p ON p.id = pex.program_entity_id + WHERE ex.external_reference_source = 'breedinginsight.org/programs' AND ex2.external_reference_source = 'breedinginsight.org') pquery +WHERE id = pquery.germplasm_id; + +-- Assertion to ensure all germplasm are now associated with a program +DO $$ + DECLARE + germs_without_program_count integer; + + BEGIN + SELECT COUNT(*) + INTO germs_without_program_count + FROM germplasm g + WHERE g.program_id IS NULL; + + IF germs_without_program_count <> 0 THEN + RAISE EXCEPTION + 'V006.003 After migration, expected all germplasm rows to have program_id set, but there are % rows with program_id not set', + germs_without_program_count; + END IF; + END +$$; \ No newline at end of file