From 3640e5d6b221ea64c296f1deb581d6b247718fbf Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Thu, 7 May 2026 15:01:45 -0400 Subject: [PATCH 01/56] Add new filter/sort models --- src/main/java/io/swagger/model/FilterBy.java | 25 +++++++++++++++ .../java/io/swagger/model/SearchRequest.java | 7 +++++ .../java/io/swagger/model/core/SortBy.java | 2 ++ .../model/core/StudySearchRequest.java | 2 ++ .../model/core/TrialSearchRequest.java | 23 +------------- .../java/io/swagger/model/sort/SortBy.java | 31 +++++++++++++++++++ .../model/{core => sort}/SortOrder.java | 2 +- .../service/SearchQueryBuilder.java | 2 +- .../service/core/StudyService.java | 3 +- .../service/core/TrialService.java | 1 + 10 files changed, 72 insertions(+), 26 deletions(-) create mode 100644 src/main/java/io/swagger/model/FilterBy.java create mode 100644 src/main/java/io/swagger/model/sort/SortBy.java rename src/main/java/io/swagger/model/{core => sort}/SortOrder.java (95%) 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..a34afb7d --- /dev/null +++ b/src/main/java/io/swagger/model/FilterBy.java @@ -0,0 +1,25 @@ +package io.swagger.model; + +import io.swagger.model.sort.SortOrder; + +public class FilterBy { + private String filterOn; + private boolean addInfoColumn = false; + + public String getFilterOn() { + return filterOn; + } + + public void setFilterOn(String filterOn) { + this.filterOn = filterOn; + } + + public boolean isAddInfoColumn() { + return addInfoColumn; + } + + public void setAddInfoColumn(boolean addInfoColumn) { + this.addInfoColumn = addInfoColumn; + } + +} diff --git a/src/main/java/io/swagger/model/SearchRequest.java b/src/main/java/io/swagger/model/SearchRequest.java index 8ad217c4..ae723e1c 100644 --- a/src/main/java/io/swagger/model/SearchRequest.java +++ b/src/main/java/io/swagger/model/SearchRequest.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.model.sort.SortBy; import java.util.ArrayList; import java.util.List; @@ -25,6 +26,12 @@ public abstract class SearchRequest { @JsonProperty("externalReferenceSources") protected List externalReferenceSources = null; + @JsonProperty("filterBy") + protected FilterBy filterBy = null; + + @JsonProperty("sortBy") + protected List sortBy = null; + final public SearchRequest page(Integer page) { this.page = page; return this; diff --git a/src/main/java/io/swagger/model/core/SortBy.java b/src/main/java/io/swagger/model/core/SortBy.java index 8a132a3e..bf215cc5 100644 --- a/src/main/java/io/swagger/model/core/SortBy.java +++ b/src/main/java/io/swagger/model/core/SortBy.java @@ -3,6 +3,8 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; +@Deprecated +// TODO: Phase out in favor of io.swagger.model.sort objects public enum SortBy { STUDYDBID("studyDbId"), diff --git a/src/main/java/io/swagger/model/core/StudySearchRequest.java b/src/main/java/io/swagger/model/core/StudySearchRequest.java index 0b26436b..26849475 100644 --- a/src/main/java/io/swagger/model/core/StudySearchRequest.java +++ b/src/main/java/io/swagger/model/core/StudySearchRequest.java @@ -5,6 +5,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.model.SearchRequest; +import io.swagger.model.sort.SortOrder; + import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/io/swagger/model/core/TrialSearchRequest.java b/src/main/java/io/swagger/model/core/TrialSearchRequest.java index 830dde51..5a4b741f 100644 --- a/src/main/java/io/swagger/model/core/TrialSearchRequest.java +++ b/src/main/java/io/swagger/model/core/TrialSearchRequest.java @@ -3,6 +3,7 @@ import java.util.Objects; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.model.SearchRequest; + import java.util.ArrayList; import java.util.List; import java.time.LocalDate; @@ -50,28 +51,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; 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..8501bd88 --- /dev/null +++ b/src/main/java/io/swagger/model/sort/SortBy.java @@ -0,0 +1,31 @@ +package io.swagger.model.sort; + +public class SortBy { + private String sortedOn; + private SortOrder sortOrder = SortOrder.ASC; + private boolean addInfoColumn = false; + + 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; + } + + public boolean isAddInfoColumn() { + return addInfoColumn; + } + + public void setAddInfoColumn(boolean addInfoColumn) { + this.addInfoColumn = addInfoColumn; + } +} diff --git a/src/main/java/io/swagger/model/core/SortOrder.java b/src/main/java/io/swagger/model/sort/SortOrder.java similarity index 95% rename from src/main/java/io/swagger/model/core/SortOrder.java rename to src/main/java/io/swagger/model/sort/SortOrder.java index 0e5b36bd..4cc9d5aa 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; 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..74ef4677 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -7,7 +7,7 @@ import java.time.OffsetDateTime; import io.swagger.model.GeoJSONSearchArea; -import io.swagger.model.core.SortOrder; +import io.swagger.model.sort.SortOrder; public class SearchQueryBuilder { 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..4c0cb198 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 @@ -6,7 +6,6 @@ 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; @@ -39,7 +38,7 @@ 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; 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..36ac0b0a 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,7 @@ import java.util.stream.Collectors; import io.swagger.model.core.*; +import io.swagger.model.sort.SortOrder; import jakarta.validation.Valid; import org.brapi.test.BrAPITestServer.exceptions.BatchDeleteWrongTypeException; From fd641789e287f6d39342d8d4763db452d6117f01 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Thu, 7 May 2026 18:57:31 -0400 Subject: [PATCH 02/56] Preliminary generic sortBy implementation on searchRequests --- src/main/java/io/swagger/model/FilterBy.java | 21 ++++-- .../java/io/swagger/model/SearchRequest.java | 20 +++++- .../sort/{SortBy.java => SortByEntry.java} | 11 +++- .../service/SearchQueryBuilder.java | 64 ++++++++++++++++++- .../service/core/TrialService.java | 42 ++---------- 5 files changed, 111 insertions(+), 47 deletions(-) rename src/main/java/io/swagger/model/sort/{SortBy.java => SortByEntry.java} (63%) diff --git a/src/main/java/io/swagger/model/FilterBy.java b/src/main/java/io/swagger/model/FilterBy.java index a34afb7d..6d72b821 100644 --- a/src/main/java/io/swagger/model/FilterBy.java +++ b/src/main/java/io/swagger/model/FilterBy.java @@ -1,17 +1,24 @@ package io.swagger.model; -import io.swagger.model.sort.SortOrder; - public class FilterBy { - private String filterOn; + private String filterColumn; + private String value; private boolean addInfoColumn = false; - public String getFilterOn() { - return filterOn; + public String getFilterColumn() { + return filterColumn; + } + + public void setFilterColumn(String filterColumn) { + this.filterColumn = filterColumn; + } + + public String getValue() { + return value; } - public void setFilterOn(String filterOn) { - this.filterOn = filterOn; + public void setValue(String value) { + this.value = value; } public boolean isAddInfoColumn() { diff --git a/src/main/java/io/swagger/model/SearchRequest.java b/src/main/java/io/swagger/model/SearchRequest.java index ae723e1c..6d999d8a 100644 --- a/src/main/java/io/swagger/model/SearchRequest.java +++ b/src/main/java/io/swagger/model/SearchRequest.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.model.sort.SortBy; +import io.swagger.model.sort.SortByEntry; import java.util.ArrayList; import java.util.List; @@ -30,7 +30,7 @@ public abstract class SearchRequest { protected FilterBy filterBy = null; @JsonProperty("sortBy") - protected List sortBy = null; + protected List sortBy = null; final public SearchRequest page(Integer page) { this.page = page; @@ -126,4 +126,20 @@ public void addExternalReferenceItem(String externalReferenceId, String external } } + + public FilterBy getFilterBy() { + return filterBy; + } + + public void setFilterBy(FilterBy filterBy) { + this.filterBy = filterBy; + } + + public List getSortByEntry() { + return sortBy; + } + + public void setSortByEntry(List sortBy) { + this.sortBy = sortBy; + } } diff --git a/src/main/java/io/swagger/model/sort/SortBy.java b/src/main/java/io/swagger/model/sort/SortByEntry.java similarity index 63% rename from src/main/java/io/swagger/model/sort/SortBy.java rename to src/main/java/io/swagger/model/sort/SortByEntry.java index 8501bd88..3d7fee7f 100644 --- a/src/main/java/io/swagger/model/sort/SortBy.java +++ b/src/main/java/io/swagger/model/sort/SortByEntry.java @@ -1,10 +1,19 @@ package io.swagger.model.sort; -public class SortBy { +// TODO: Replace io.swagger.model.core.SortBy with this class and rename this class to SortBy +public class SortByEntry { private String sortedOn; private SortOrder sortOrder = SortOrder.ASC; private boolean addInfoColumn = false; + public SortByEntry(String sortedOn, + SortOrder sortOrder, + boolean addInfoColumn) { + this.sortedOn = sortedOn; + this.sortOrder = sortOrder; + this.addInfoColumn = addInfoColumn; + } + public String getSortedOn() { return sortedOn; } 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 74ef4677..67ab3378 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -6,7 +6,9 @@ import java.time.LocalDate; import java.time.OffsetDateTime; +import io.swagger.model.FilterBy; import io.swagger.model.GeoJSONSearchArea; +import io.swagger.model.sort.SortByEntry; import io.swagger.model.sort.SortOrder; public class SearchQueryBuilder { @@ -311,6 +313,8 @@ private String paramFilter(String param) { return param.replace('.', '_').replace('*', '_'); } + @Deprecated + // Use withSortBy instead public SearchQueryBuilder withSort(String sortByStr, SortOrder sortOrder) { String sortOrderStr = "ASC"; if (sortOrder != null) { @@ -321,4 +325,62 @@ public SearchQueryBuilder withSort(String sortByStr, SortOrder sortOrder) { return this; } -} + + /** + * Takes a list of SortBy options that should typically come in a searchRequest. + * Applies the entries in the list to sort the SearchQuery. + * + * A SortBy has + * - A column name + * - An order (DESC, ASC) + * - A boolean denoting whether the column to be sorted is data stored in additional info + */ + public SearchQueryBuilder sortBy(List sortBy) { + + if (sortBy == null || sortBy.isEmpty()) { + return this; + } + + for (SortByEntry sort : sortBy) { + if (sortBy.getFirst().equals(sort)) { + this.sortClause += " ORDER BY "; + buildSort(sort); + } + + this.sortClause += ", "; + buildSort(sort); + } + + return this; + } + + private void buildSort(SortByEntry sort) { + if (sort.isAddInfoColumn()) { + // TODO: This assumes the jsonb value of the key is always text. Might need to support numerical sort. + this.sortClause += "additional_info ->>" + sort.getSortedOn() + " " + sort.getSortOrder() + " "; + } else { + this.sortClause += sort.getSortedOn() + " " + sort.getSortOrder() + " "; + } + } + + /** + * Takes a list of FilterBy options that should typically come in a searchRequest. + * Applies the entries in the list to filter the SearchQuery. + * + * A FilterBy has + * - A column name + * - A boolean denoting whether the column to be sorted is data stored in additional info + */ + public SearchQueryBuilder filterBy(List filterBy) { + for (FilterBy filter : filterBy) { + if (filter.isAddInfoColumn()) { + // TODO: This assumes the jsonb value of the key is always text. Might need to support numerical sort. + this.whereClause += " AND additional_info ->> " + filter.getFilterColumn() + " = " + filter.getValue() + " "; + } + else { + this.whereClause += " AND additional_info ->> " + filter.getFilterColumn() + " " + filter.getValue() + " "; + } + } + + return this; + }} 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 36ac0b0a..a9ee35cc 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,7 @@ import java.util.stream.Collectors; import io.swagger.model.core.*; +import io.swagger.model.sort.SortByEntry; import io.swagger.model.sort.SortOrder; import jakarta.validation.Valid; @@ -95,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) { + SortByEntry querySortBy = new SortByEntry(sortBy, SortOrder.valueOf(sortOrder), false); + request.setSortByEntry(List.of()); + } request.addExternalReferenceItem(externalReferenceId, externalReferenceID, externalReferenceSource); return findTrials(request, metadata); } @@ -127,7 +127,7 @@ public List findTrials(@Valid TrialSearchRequest request, Metadata metada .appendList(request.getStudyNames(), "*study.studyName").appendList(request.getTrialDbIds(), "id") .appendList(request.getTrialNames(), "trialName") .appendDateRange(request.getSearchDateRangeStart(), request.getSearchDateRangeEnd(), "startDate") - .withSort(getSortByField(request.getSortBy()), request.getSortOrder()); + .sortBy(request.getSortByEntry()); Page trialsPage = trialRepository.findAllBySearchAndPaginate(searchQuery, pageReq); PagingUtility.calculateMetaData(metadata, trialsPage); @@ -357,34 +357,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; - } - } From 32664a2162b3757df800c9fd50ad7d8fb02b3ac8 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Thu, 21 May 2026 16:06:21 -0400 Subject: [PATCH 03/56] Use @Formula to derive needed addInfo entity attrs, update SearchQueryBuilder --- src/main/java/io/swagger/model/FilterBy.java | 9 ----- .../java/io/swagger/model/SearchRequest.java | 8 ++-- .../{SortByEntry.java => SortByElement.java} | 20 +++------- .../model/entity/core/TrialEntity.java | 8 ++++ .../service/SearchQueryBuilder.java | 37 ++++++++----------- .../service/core/TrialService.java | 6 +-- 6 files changed, 36 insertions(+), 52 deletions(-) rename src/main/java/io/swagger/model/sort/{SortByEntry.java => SortByElement.java} (58%) diff --git a/src/main/java/io/swagger/model/FilterBy.java b/src/main/java/io/swagger/model/FilterBy.java index 6d72b821..001ea05b 100644 --- a/src/main/java/io/swagger/model/FilterBy.java +++ b/src/main/java/io/swagger/model/FilterBy.java @@ -3,7 +3,6 @@ public class FilterBy { private String filterColumn; private String value; - private boolean addInfoColumn = false; public String getFilterColumn() { return filterColumn; @@ -21,12 +20,4 @@ public void setValue(String value) { this.value = value; } - public boolean isAddInfoColumn() { - return addInfoColumn; - } - - public void setAddInfoColumn(boolean addInfoColumn) { - this.addInfoColumn = addInfoColumn; - } - } diff --git a/src/main/java/io/swagger/model/SearchRequest.java b/src/main/java/io/swagger/model/SearchRequest.java index 6d999d8a..9d9695c8 100644 --- a/src/main/java/io/swagger/model/SearchRequest.java +++ b/src/main/java/io/swagger/model/SearchRequest.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.model.sort.SortByEntry; +import io.swagger.model.sort.SortByElement; import java.util.ArrayList; import java.util.List; @@ -30,7 +30,7 @@ public abstract class SearchRequest { protected FilterBy filterBy = null; @JsonProperty("sortBy") - protected List sortBy = null; + protected List sortBy = null; final public SearchRequest page(Integer page) { this.page = page; @@ -135,11 +135,11 @@ public void setFilterBy(FilterBy filterBy) { this.filterBy = filterBy; } - public List getSortByEntry() { + public List getSortByEntry() { return sortBy; } - public void setSortByEntry(List sortBy) { + public void setSortByEntry(List sortBy) { this.sortBy = sortBy; } } diff --git a/src/main/java/io/swagger/model/sort/SortByEntry.java b/src/main/java/io/swagger/model/sort/SortByElement.java similarity index 58% rename from src/main/java/io/swagger/model/sort/SortByEntry.java rename to src/main/java/io/swagger/model/sort/SortByElement.java index 3d7fee7f..cbc79600 100644 --- a/src/main/java/io/swagger/model/sort/SortByEntry.java +++ b/src/main/java/io/swagger/model/sort/SortByElement.java @@ -1,19 +1,19 @@ package io.swagger.model.sort; // TODO: Replace io.swagger.model.core.SortBy with this class and rename this class to SortBy -public class SortByEntry { +public class SortByElement { private String sortedOn; private SortOrder sortOrder = SortOrder.ASC; - private boolean addInfoColumn = false; - public SortByEntry(String sortedOn, - SortOrder sortOrder, - boolean addInfoColumn) { + public SortByElement(String sortedOn, + SortOrder sortOrder, + boolean addInfoColumn) { this.sortedOn = sortedOn; this.sortOrder = sortOrder; - this.addInfoColumn = addInfoColumn; } + public SortByElement() {} + public String getSortedOn() { return sortedOn; } @@ -29,12 +29,4 @@ public SortOrder getSortOrder() { public void setSortOrder(SortOrder sortOrder) { this.sortOrder = sortOrder; } - - public boolean isAddInfoColumn() { - return addInfoColumn; - } - - public void setAddInfoColumn(boolean addInfoColumn) { - this.addInfoColumn = addInfoColumn; - } } 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..f18543fc 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("(to_timestamp(additional_info #>> '{createdDate}', 'YYYY-MM-DD'))") + private OffsetDateTime 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/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index 67ab3378..41a423ec 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -8,7 +8,7 @@ import io.swagger.model.FilterBy; import io.swagger.model.GeoJSONSearchArea; -import io.swagger.model.sort.SortByEntry; +import io.swagger.model.sort.SortByElement; import io.swagger.model.sort.SortOrder; public class SearchQueryBuilder { @@ -22,8 +22,8 @@ public class SearchQueryBuilder { private Class clazz; public SearchQueryBuilder(Class clazz) { - this.selectClause = "SELECT distinct entity FROM " + clazz.getSimpleName() + " entity "; - this.selectOnlyIds = "SELECT distinct entity.id FROM " + clazz.getSimpleName() + " entity "; + this.selectClause = "SELECT entity FROM " + clazz.getSimpleName() + " entity "; + this.selectOnlyIds = "SELECT entity.id FROM " + clazz.getSimpleName() + " entity "; this.whereClause = "WHERE 1=1 "; this.defaultSort = " ORDER BY entity.id ASC "; this.sortClause = ""; @@ -307,6 +307,10 @@ private String entityPrefix(String field) { } } + private String addInfoPrefix(String field) { + return "function('jsonb_extract_path_text', entity.additionalInfo, '" + field + "' ) "; + } + private String paramFilter(String param) { if (param == null) return ""; @@ -335,32 +339,27 @@ public SearchQueryBuilder withSort(String sortByStr, SortOrder sortOrder) { * - An order (DESC, ASC) * - A boolean denoting whether the column to be sorted is data stored in additional info */ - public SearchQueryBuilder sortBy(List sortBy) { + public SearchQueryBuilder sortBy(List sortBy) { if (sortBy == null || sortBy.isEmpty()) { return this; } - for (SortByEntry sort : sortBy) { + for (SortByElement sort : sortBy) { if (sortBy.getFirst().equals(sort)) { this.sortClause += " ORDER BY "; buildSort(sort); + } else { + this.sortClause += ", "; + buildSort(sort); } - - this.sortClause += ", "; - buildSort(sort); } return this; } - private void buildSort(SortByEntry sort) { - if (sort.isAddInfoColumn()) { - // TODO: This assumes the jsonb value of the key is always text. Might need to support numerical sort. - this.sortClause += "additional_info ->>" + sort.getSortedOn() + " " + sort.getSortOrder() + " "; - } else { - this.sortClause += sort.getSortedOn() + " " + sort.getSortOrder() + " "; - } + private void buildSort(SortByElement sort) { + this.sortClause += entityPrefix(sort.getSortedOn()) + " " + sort.getSortOrder() + " "; } /** @@ -373,13 +372,7 @@ private void buildSort(SortByEntry sort) { */ public SearchQueryBuilder filterBy(List filterBy) { for (FilterBy filter : filterBy) { - if (filter.isAddInfoColumn()) { - // TODO: This assumes the jsonb value of the key is always text. Might need to support numerical sort. - this.whereClause += " AND additional_info ->> " + filter.getFilterColumn() + " = " + filter.getValue() + " "; - } - else { - this.whereClause += " AND additional_info ->> " + filter.getFilterColumn() + " " + filter.getValue() + " "; - } + this.whereClause += " AND " + entityPrefix(filter.getFilterColumn()) + " LIKE '%" + filter.getValue() + "%' "; } return this; 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 a9ee35cc..6217f755 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,7 +4,7 @@ import java.util.stream.Collectors; import io.swagger.model.core.*; -import io.swagger.model.sort.SortByEntry; +import io.swagger.model.sort.SortByElement; import io.swagger.model.sort.SortOrder; import jakarta.validation.Valid; @@ -97,8 +97,8 @@ public List findTrials(@Valid String commonCropName, @Valid String contac if (searchDateRangeEnd != null) request.setSearchDateRangeEnd(searchDateRangeEnd); if (sortBy != null) { - SortByEntry querySortBy = new SortByEntry(sortBy, SortOrder.valueOf(sortOrder), false); - request.setSortByEntry(List.of()); + SortByElement querySortBy = new SortByElement(sortBy, SortOrder.valueOf(sortOrder), false); + request.setSortByEntry(List.of(querySortBy)); } request.addExternalReferenceItem(externalReferenceId, externalReferenceID, externalReferenceSource); return findTrials(request, metadata); From 2d5128502765c616c7328dcf7331bc859a2dbac9 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Thu, 21 May 2026 19:22:15 -0400 Subject: [PATCH 04/56] Preliminary pass at allowed fields mapping --- .../java/io/swagger/model/SearchRequest.java | 62 +++++++++++++++++-- .../model/core/TrialSearchRequest.java | 20 +++++- 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/swagger/model/SearchRequest.java b/src/main/java/io/swagger/model/SearchRequest.java index 9d9695c8..cb29ee2c 100644 --- a/src/main/java/io/swagger/model/SearchRequest.java +++ b/src/main/java/io/swagger/model/SearchRequest.java @@ -3,9 +3,12 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.model.sort.SortByElement; +import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; +import org.springframework.http.HttpStatus; import java.util.ArrayList; import java.util.List; +import java.util.Map; public abstract class SearchRequest { @JsonIgnore @@ -27,11 +30,19 @@ public abstract class SearchRequest { protected List externalReferenceSources = null; @JsonProperty("filterBy") - protected FilterBy filterBy = null; + 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; @@ -127,11 +138,30 @@ public void addExternalReferenceItem(String externalReferenceId, String external } - public FilterBy getFilterBy() { + public List getFilterBy() { return filterBy; } - public void setFilterBy(FilterBy filterBy) { + public void setFilterBy(List filterBy) throws BrAPIServerException { + + if (filterBy == null || filterBy.isEmpty()) { + return; + } + + Map allowedSortFilterNames = getSortFilterEntityColumnNamesByRequestName(); + + for (FilterBy filterByItem : filterBy) { + String filterColumnEntityName = getSortFilterEntityColumnNamesByRequestName().get(filterByItem.getFilterColumn()); + + if (filterColumnEntityName == null) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, + String.format("Supplied filterColumn [%s] not available in allowed names [%s]", filterByItem.getFilterColumn(), allowedSortFilterNames.keySet()) + ); + } else { + // Remap suppliedFilterColumn to actual entity name supplied by mapper + filterByItem.setFilterColumn(filterColumnEntityName); + } + } this.filterBy = filterBy; } @@ -139,7 +169,31 @@ public List getSortByEntry() { return sortBy; } - public void setSortByEntry(List sortBy) { + public void setSortByEntry(List sortBy) throws BrAPIServerException { + + if (sortBy == null || sortBy.isEmpty()) { + return; + } + + Map allowedSortFilterNames = getSortFilterEntityColumnNamesByRequestName(); + + for (SortByElement sortByItem : sortBy) { + String filterColumnEntityName = getSortFilterEntityColumnNamesByRequestName().get(sortByItem.getSortedOn()); + + if (filterColumnEntityName == null) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, + String.format("Supplied sortColumn [%s] not available in allowed names [%s]", sortByItem.getSortedOn(), allowedSortFilterNames.keySet()) + ); + } else { + // Remap suppliedFilterColumn to actual entity name supplied by mapper + sortByItem.setSortedOn(filterColumnEntityName); + } + } + this.sortBy = sortBy; } + + public Map getSortFilterEntityColumnNamesByRequestName() { + throw new UnsupportedOperationException(String.format("Sort/Filtering not implemented for %s", this.getClass().getSimpleName())); + } } diff --git a/src/main/java/io/swagger/model/core/TrialSearchRequest.java b/src/main/java/io/swagger/model/core/TrialSearchRequest.java index 5a4b741f..324774fa 100644 --- a/src/main/java/io/swagger/model/core/TrialSearchRequest.java +++ b/src/main/java/io/swagger/model/core/TrialSearchRequest.java @@ -1,14 +1,23 @@ 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 java.time.LocalDate; public class TrialSearchRequest extends SearchRequest { + + // Key - allowed sort or field filter name for this entity + // Value = entity field name that represents the submitted field. Used later on in query building. + private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = + Map.of( + "trialName", "trialName", + "createdDate", "createdDate", + "createdBy", "createdBy" + ); + @JsonProperty("commonCropNames") private List commonCropNames = null; @@ -424,4 +433,9 @@ public Integer getTotalParameterCount() { count += this.trialPUIs.size(); return count; } + + @Override + public Map getSortFilterEntityColumnNamesByRequestName() { + return ALLOWED_SORT_AND_FILTER_FIELDS; + } } From f9440f06ea342f59e25b9fdb8ff8846eb8acb434 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Fri, 22 May 2026 15:42:16 -0400 Subject: [PATCH 05/56] Add parametrization logic for filter values, infrastructure for 400 errors on serialization --- src/main/java/io/swagger/model/FilterBy.java | 11 +++--- .../java/io/swagger/model/SearchRequest.java | 32 ++++++++++++---- .../BrapiExceptionHandler.java | 37 +++++++++++++++++-- .../model/entity/core/TrialEntity.java | 4 +- .../service/SearchQueryBuilder.java | 28 +++++++++++--- .../service/core/TrialService.java | 6 ++- 6 files changed, 92 insertions(+), 26 deletions(-) diff --git a/src/main/java/io/swagger/model/FilterBy.java b/src/main/java/io/swagger/model/FilterBy.java index 001ea05b..acbc03e7 100644 --- a/src/main/java/io/swagger/model/FilterBy.java +++ b/src/main/java/io/swagger/model/FilterBy.java @@ -1,15 +1,16 @@ package io.swagger.model; public class FilterBy { - private String filterColumn; + + private String filterOn; private String value; - public String getFilterColumn() { - return filterColumn; + public String getFilterOn() { + return filterOn; } - public void setFilterColumn(String filterColumn) { - this.filterColumn = filterColumn; + public void setFilterOn(String filterOn) { + this.filterOn = filterOn; } public String getValue() { diff --git a/src/main/java/io/swagger/model/SearchRequest.java b/src/main/java/io/swagger/model/SearchRequest.java index cb29ee2c..cefd14bd 100644 --- a/src/main/java/io/swagger/model/SearchRequest.java +++ b/src/main/java/io/swagger/model/SearchRequest.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; import io.swagger.model.sort.SortByElement; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; import org.springframework.http.HttpStatus; @@ -142,6 +143,7 @@ public List getFilterBy() { return filterBy; } + @JsonSetter("filterBy") public void setFilterBy(List filterBy) throws BrAPIServerException { if (filterBy == null || filterBy.isEmpty()) { @@ -151,25 +153,35 @@ public void setFilterBy(List filterBy) throws BrAPIServerException { Map allowedSortFilterNames = getSortFilterEntityColumnNamesByRequestName(); for (FilterBy filterByItem : filterBy) { - String filterColumnEntityName = getSortFilterEntityColumnNamesByRequestName().get(filterByItem.getFilterColumn()); + + 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."); + } + + String filterColumnEntityName = getSortFilterEntityColumnNamesByRequestName().get(filterByItem.getFilterOn()); if (filterColumnEntityName == null) { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, - String.format("Supplied filterColumn [%s] not available in allowed names [%s]", filterByItem.getFilterColumn(), allowedSortFilterNames.keySet()) + String.format("Supplied filterColumn [%s] not available in allowed names [%s]", filterByItem.getFilterOn(), allowedSortFilterNames.keySet()) ); } else { // Remap suppliedFilterColumn to actual entity name supplied by mapper - filterByItem.setFilterColumn(filterColumnEntityName); + filterByItem.setFilterOn(filterColumnEntityName); } } this.filterBy = filterBy; } - public List getSortByEntry() { + public List getSortByElements() { return sortBy; } - public void setSortByEntry(List sortBy) throws BrAPIServerException { + @JsonSetter("sortBy") + public void setSortByElements(List sortBy) throws BrAPIServerException { if (sortBy == null || sortBy.isEmpty()) { return; @@ -178,15 +190,19 @@ public void setSortByEntry(List sortBy) throws BrAPIServerExcepti Map allowedSortFilterNames = getSortFilterEntityColumnNamesByRequestName(); for (SortByElement sortByItem : sortBy) { - String filterColumnEntityName = getSortFilterEntityColumnNamesByRequestName().get(sortByItem.getSortedOn()); + if (sortByItem.getSortedOn() == null || sortByItem.getSortedOn().isEmpty()) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "sortedOn attribute not provided in element of sortBy list"); + } - if (filterColumnEntityName == null) { + String sortColumnEntityName = getSortFilterEntityColumnNamesByRequestName().get(sortByItem.getSortedOn()); + + if (sortColumnEntityName == null) { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, String.format("Supplied sortColumn [%s] not available in allowed names [%s]", sortByItem.getSortedOn(), allowedSortFilterNames.keySet()) ); } else { // Remap suppliedFilterColumn to actual entity name supplied by mapper - sortByItem.setSortedOn(filterColumnEntityName); + sortByItem.setSortedOn(sortColumnEntityName); } } 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/model/entity/core/TrialEntity.java b/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/TrialEntity.java index f18543fc..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 @@ -40,8 +40,8 @@ public class TrialEntity extends BrAPIPrimaryEntity { @Column(name = "soft_deleted") private boolean softDeleted; - @Formula("(to_timestamp(additional_info #>> '{createdDate}', 'YYYY-MM-DD'))") - private OffsetDateTime createdDate; + @Formula("(additional_info #>> '{createdDate}')") + private String createdDate; @Formula("(additional_info #>> '{createdBy,userName}')") private String createdBy; 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 41a423ec..86f4f37e 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -10,6 +10,8 @@ import io.swagger.model.GeoJSONSearchArea; import io.swagger.model.sort.SortByElement; import io.swagger.model.sort.SortOrder; +import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; +import org.springframework.http.HttpStatus; public class SearchQueryBuilder { @@ -135,6 +137,16 @@ public SearchQueryBuilder appendSingle(UUID single, String columnName) { return this; } + public SearchQueryBuilder appendLike(String like, String columnName) { + String paramName = paramFilter(columnName); + + if (like != null) { + this.whereClause += "AND " + entityPrefix(columnName) + " LIKE :" + paramName + " "; + this.params.put(paramName, "%" + like + "%"); + } + return this; + } + public > SearchQueryBuilder appendEnum(E enumVal, String columnName) { String paramName = paramFilter(columnName); if (enumVal != null) { @@ -337,9 +349,8 @@ public SearchQueryBuilder withSort(String sortByStr, SortOrder sortOrder) { * A SortBy has * - A column name * - An order (DESC, ASC) - * - A boolean denoting whether the column to be sorted is data stored in additional info */ - public SearchQueryBuilder sortBy(List sortBy) { + public SearchQueryBuilder sortBy(List sortBy) throws BrAPIServerException { if (sortBy == null || sortBy.isEmpty()) { return this; @@ -368,12 +379,17 @@ private void buildSort(SortByElement sort) { * * A FilterBy has * - A column name - * - A boolean denoting whether the column to be sorted is data stored in additional info */ - public SearchQueryBuilder filterBy(List filterBy) { + public SearchQueryBuilder filterBy(List filterBy) throws BrAPIServerException { + SearchQueryBuilder searchQuery = this; + + if (filterBy == null || filterBy.isEmpty()) { + return searchQuery; + } + for (FilterBy filter : filterBy) { - this.whereClause += " AND " + entityPrefix(filter.getFilterColumn()) + " LIKE '%" + filter.getValue() + "%' "; + searchQuery = appendLike(filter.getValue(), filter.getFilterOn()); } - return this; + return searchQuery; }} 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 6217f755..c01622d4 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 @@ -97,8 +97,9 @@ public List findTrials(@Valid String commonCropName, @Valid String contac if (searchDateRangeEnd != null) request.setSearchDateRangeEnd(searchDateRangeEnd); if (sortBy != null) { + // TODO: Fix this use case by adding allowable sort fields for trials from old enum in here SortByElement querySortBy = new SortByElement(sortBy, SortOrder.valueOf(sortOrder), false); - request.setSortByEntry(List.of(querySortBy)); + request.setSortByElements(List.of(querySortBy)); } request.addExternalReferenceItem(externalReferenceId, externalReferenceID, externalReferenceSource); return findTrials(request, metadata); @@ -127,7 +128,8 @@ public List findTrials(@Valid TrialSearchRequest request, Metadata metada .appendList(request.getStudyNames(), "*study.studyName").appendList(request.getTrialDbIds(), "id") .appendList(request.getTrialNames(), "trialName") .appendDateRange(request.getSearchDateRangeStart(), request.getSearchDateRangeEnd(), "startDate") - .sortBy(request.getSortByEntry()); + .sortBy(request.getSortByElements()) + .filterBy(request.getFilterBy()); Page trialsPage = trialRepository.findAllBySearchAndPaginate(searchQuery, pageReq); PagingUtility.calculateMetaData(metadata, trialsPage); From 90214e06326e2f204b645df7bf45f861e5e92071 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Fri, 22 May 2026 17:06:44 -0400 Subject: [PATCH 06/56] Use new SortBy class in all use cases. --- .../java/io/swagger/model/SearchRequest.java | 12 ++-- .../java/io/swagger/model/core/SortBy.java | 58 ----------------- .../model/core/StudySearchRequest.java | 63 ++++++++----------- .../model/core/TrialSearchRequest.java | 6 +- .../sort/{SortByElement.java => SortBy.java} | 10 ++- .../service/SearchQueryBuilder.java | 9 ++- .../service/core/StudyService.java | 56 +++-------------- .../service/core/TrialService.java | 7 +-- 8 files changed, 53 insertions(+), 168 deletions(-) delete mode 100644 src/main/java/io/swagger/model/core/SortBy.java rename src/main/java/io/swagger/model/sort/{SortByElement.java => SortBy.java} (63%) diff --git a/src/main/java/io/swagger/model/SearchRequest.java b/src/main/java/io/swagger/model/SearchRequest.java index cefd14bd..828cd69c 100644 --- a/src/main/java/io/swagger/model/SearchRequest.java +++ b/src/main/java/io/swagger/model/SearchRequest.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import io.swagger.model.sort.SortByElement; +import io.swagger.model.sort.SortBy; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; import org.springframework.http.HttpStatus; @@ -34,7 +34,7 @@ public abstract class SearchRequest { protected List filterBy = null; @JsonProperty("sortBy") - protected List sortBy = null; + protected List sortBy = null; @JsonIgnore protected Map sortFilterEntityColumnNamesByRequestName = null; @@ -143,7 +143,6 @@ public List getFilterBy() { return filterBy; } - @JsonSetter("filterBy") public void setFilterBy(List filterBy) throws BrAPIServerException { if (filterBy == null || filterBy.isEmpty()) { @@ -176,12 +175,11 @@ public void setFilterBy(List filterBy) throws BrAPIServerException { this.filterBy = filterBy; } - public List getSortByElements() { + public List getSortByElements() { return sortBy; } - @JsonSetter("sortBy") - public void setSortByElements(List sortBy) throws BrAPIServerException { + public void setSortBy(List sortBy) throws BrAPIServerException { if (sortBy == null || sortBy.isEmpty()) { return; @@ -189,7 +187,7 @@ public void setSortByElements(List sortBy) throws BrAPIServerExce Map allowedSortFilterNames = getSortFilterEntityColumnNamesByRequestName(); - for (SortByElement sortByItem : sortBy) { + 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"); } 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 bf215cc5..00000000 --- a/src/main/java/io/swagger/model/core/SortBy.java +++ /dev/null @@ -1,58 +0,0 @@ -package io.swagger.model.core; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - -@Deprecated -// TODO: Phase out in favor of io.swagger.model.sort objects -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 26849475..78ed90d0 100644 --- a/src/main/java/io/swagger/model/core/StudySearchRequest.java +++ b/src/main/java/io/swagger/model/core/StudySearchRequest.java @@ -1,16 +1,34 @@ 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 io.swagger.model.sort.SortOrder; 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 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", "*obsunit.germplasm.id"), + Map.entry("locationDbId", "location.id"), + Map.entry("observationVariableDbId", "*observation.observationVariable.id"), + Map.entry("programDbId", "trial.program.id"), + Map.entry("programName", "trial.program.name"), + Map.entry("seasonDbId", "*season.id"), + Map.entry("studyDbId", "id"), + Map.entry("studyLocation", "location.id"), + Map.entry("trialDbId", "trial.id"), + Map.entry("studyType", "studyName"), + Map.entry("studyName", "studyName") + ); + @JsonProperty("commonCropNames") private List commonCropNames = null; @@ -56,12 +74,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; @@ -378,32 +390,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; @@ -494,7 +480,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); @@ -505,7 +490,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 @@ -531,7 +516,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"); @@ -585,8 +569,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) @@ -595,4 +577,9 @@ public Integer getTotalParameterCount() { count += this.studyTypes.size(); return count; } + + @Override + public Map getSortFilterEntityColumnNamesByRequestName() { + 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 324774fa..aaed24eb 100644 --- a/src/main/java/io/swagger/model/core/TrialSearchRequest.java +++ b/src/main/java/io/swagger/model/core/TrialSearchRequest.java @@ -15,7 +15,11 @@ public class TrialSearchRequest extends SearchRequest { Map.of( "trialName", "trialName", "createdDate", "createdDate", - "createdBy", "createdBy" + "createdBy", "createdBy", + "trialDbId", "id", + "programDbId","program.id", + "startDate", "startDate", + "endDate", "endDate" ); @JsonProperty("commonCropNames") diff --git a/src/main/java/io/swagger/model/sort/SortByElement.java b/src/main/java/io/swagger/model/sort/SortBy.java similarity index 63% rename from src/main/java/io/swagger/model/sort/SortByElement.java rename to src/main/java/io/swagger/model/sort/SortBy.java index cbc79600..bb286fa6 100644 --- a/src/main/java/io/swagger/model/sort/SortByElement.java +++ b/src/main/java/io/swagger/model/sort/SortBy.java @@ -1,18 +1,16 @@ package io.swagger.model.sort; -// TODO: Replace io.swagger.model.core.SortBy with this class and rename this class to SortBy -public class SortByElement { +public class SortBy { private String sortedOn; private SortOrder sortOrder = SortOrder.ASC; - public SortByElement(String sortedOn, - SortOrder sortOrder, - boolean addInfoColumn) { + public SortBy(String sortedOn, + SortOrder sortOrder) { this.sortedOn = sortedOn; this.sortOrder = sortOrder; } - public SortByElement() {} + public SortBy() {} public String getSortedOn() { return sortedOn; 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 86f4f37e..4961ed14 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -8,10 +8,9 @@ import io.swagger.model.FilterBy; import io.swagger.model.GeoJSONSearchArea; -import io.swagger.model.sort.SortByElement; +import io.swagger.model.sort.SortBy; import io.swagger.model.sort.SortOrder; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; -import org.springframework.http.HttpStatus; public class SearchQueryBuilder { @@ -350,13 +349,13 @@ public SearchQueryBuilder withSort(String sortByStr, SortOrder sortOrder) { * - A column name * - An order (DESC, ASC) */ - public SearchQueryBuilder sortBy(List sortBy) throws BrAPIServerException { + public SearchQueryBuilder sortBy(List sortBy) throws BrAPIServerException { if (sortBy == null || sortBy.isEmpty()) { return this; } - for (SortByElement sort : sortBy) { + for (SortBy sort : sortBy) { if (sortBy.getFirst().equals(sort)) { this.sortClause += " ORDER BY "; buildSort(sort); @@ -369,7 +368,7 @@ public SearchQueryBuilder sortBy(List sortBy) throws BrAPIServ return this; } - private void buildSort(SortByElement sort) { + private void buildSort(SortBy sort) { this.sortClause += entityPrefix(sort.getSortedOn()) + " " + sort.getSortOrder() + " "; } 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 4c0cb198..e5611ab7 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,6 +3,7 @@ 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; @@ -37,7 +38,6 @@ 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.sort.SortOrder; import io.swagger.model.core.Study; import io.swagger.model.core.StudyExperimentalDesign; @@ -107,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.valueOf(sortOrder)); + + request.setSortBy(List.of(sortByElement)); + } request.addExternalReferenceItem(externalReferenceId, externalReferenceID, externalReferenceSource); @@ -158,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()); Page studiesPage = studyRepository.findAllBySearchAndPaginate(searchQuery, pageReq); PagingUtility.calculateMetaData(metaData, studiesPage); @@ -565,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 c01622d4..21146a30 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,7 +4,7 @@ import java.util.stream.Collectors; import io.swagger.model.core.*; -import io.swagger.model.sort.SortByElement; +import io.swagger.model.sort.SortBy; import io.swagger.model.sort.SortOrder; import jakarta.validation.Valid; @@ -97,9 +97,8 @@ public List findTrials(@Valid String commonCropName, @Valid String contac if (searchDateRangeEnd != null) request.setSearchDateRangeEnd(searchDateRangeEnd); if (sortBy != null) { - // TODO: Fix this use case by adding allowable sort fields for trials from old enum in here - SortByElement querySortBy = new SortByElement(sortBy, SortOrder.valueOf(sortOrder), false); - request.setSortByElements(List.of(querySortBy)); + SortBy querySortBy = new SortBy(sortBy, SortOrder.valueOf(sortOrder)); + request.setSortBy(List.of(querySortBy)); } request.addExternalReferenceItem(externalReferenceId, externalReferenceID, externalReferenceSource); return findTrials(request, metadata); From 90e77dd854c47f07cfe002811b23e1ab8645e13c Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Fri, 29 May 2026 15:50:12 -0400 Subject: [PATCH 07/56] Make filter case insensitive Additionally add active as a filter column --- .../swagger/model/core/TrialSearchRequest.java | 3 ++- .../service/SearchQueryBuilder.java | 17 ++--------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/main/java/io/swagger/model/core/TrialSearchRequest.java b/src/main/java/io/swagger/model/core/TrialSearchRequest.java index aaed24eb..ce24b1f7 100644 --- a/src/main/java/io/swagger/model/core/TrialSearchRequest.java +++ b/src/main/java/io/swagger/model/core/TrialSearchRequest.java @@ -19,7 +19,8 @@ public class TrialSearchRequest extends SearchRequest { "trialDbId", "id", "programDbId","program.id", "startDate", "startDate", - "endDate", "endDate" + "endDate", "endDate", + "active", "active" ); @JsonProperty("commonCropNames") 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 4961ed14..61ca21f8 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -140,7 +140,7 @@ public SearchQueryBuilder appendLike(String like, String columnName) { String paramName = paramFilter(columnName); if (like != null) { - this.whereClause += "AND " + entityPrefix(columnName) + " LIKE :" + paramName + " "; + this.whereClause += "AND lower(" + entityPrefix(columnName) + ") LIKE :" + paramName + " "; this.params.put(paramName, "%" + like + "%"); } return this; @@ -328,19 +328,6 @@ private String paramFilter(String param) { return param.replace('.', '_').replace('*', '_'); } - @Deprecated - // Use withSortBy instead - public SearchQueryBuilder withSort(String sortByStr, SortOrder sortOrder) { - String sortOrderStr = "ASC"; - if (sortOrder != null) { - sortOrderStr = sortOrder.toString(); - } - - this.sortClause += " ORDER BY " + entityPrefix(sortByStr) + " " + sortOrderStr; - - return this; - } - /** * Takes a list of SortBy options that should typically come in a searchRequest. * Applies the entries in the list to sort the SearchQuery. @@ -387,7 +374,7 @@ public SearchQueryBuilder filterBy(List filterBy) throws BrAPIServe } for (FilterBy filter : filterBy) { - searchQuery = appendLike(filter.getValue(), filter.getFilterOn()); + searchQuery = appendLike(filter.getValue().toLowerCase(), filter.getFilterOn()); } return searchQuery; From 3bf581ada9b97adcff06bd7346a40391f5ce01a4 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 1 Jun 2026 15:48:51 -0400 Subject: [PATCH 08/56] Fix pom jackson depedency issues --- pom.xml | 54 ++++++++++++++---------------------------------------- 1 file changed, 14 insertions(+), 40 deletions(-) diff --git a/pom.xml b/pom.xml index 962d101a..a1e201c5 100644 --- a/pom.xml +++ b/pom.xml @@ -20,6 +20,7 @@ 21 2.10.0 2.0.16 + 2.18.1 @@ -73,28 +74,11 @@ io.jsonwebtoken jjwt 0.9.1 - - - com.fasterxml.jackson.core - jackson-databind - - com.google.api-client google-api-client 1.27.0 - - - com.fasterxml.jackson.core - jackson-core - - - - - com.fasterxml.jackson.core - jackson-databind - [2.9.9.1,) io.springfox @@ -106,11 +90,6 @@ springfox-swagger-ui ${springfox-version} - - com.github.joschi.jackson - jackson-datatype-threetenbp - 2.8.4 - jakarta.annotation jakarta.annotation-api @@ -143,6 +122,18 @@ + + + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + + + + @@ -161,7 +152,7 @@ org.springframework.boot spring-boot-maven-plugin - 2.7.5 + ${spring.version} @@ -173,21 +164,4 @@ - - - spring-releases - https://repo.spring.io/libs-release - - - spring-milestone - https://repo.spring.io/libs-milestone - - - - - spring-releases - https://repo.spring.io/libs-release - - - From da60a1adb17397979b52db4624c5561850aa03ee Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 21 Jul 2026 17:46:34 -0400 Subject: [PATCH 09/56] Migrate parent observation ids on ou_level to brapi dbIds --- .../V006_001__migrate_parent_ou_ids.sql | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql 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..09533d4f --- /dev/null +++ b/src/main/resources/db/migration/V006_001__migrate_parent_ou_ids.sql @@ -0,0 +1,27 @@ +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 + SELECT + id AS level_id, + substring(level_code FROM '^([^ ]+)') AS parent_ou_id + FROM observation_unit_level + WHERE level_code LIKE '% %' + ), + ou_ids_matched_on_levels AS ( + 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 external_reference ex ON ouex.external_references_id = ex.id + JOIN levels_and_parent_ou_bi_id ON ex.external_reference_id = levels_and_parent_ou_bi_id.parent_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 \ No newline at end of file From c2afc76cafd3acf1f3cc20514e2883e13c7966d1 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 21 Jul 2026 17:59:40 -0400 Subject: [PATCH 10/56] Add some clarifying comments --- .../db/migration/V006_001__migrate_parent_ou_ids.sql | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 index 09533d4f..3dd227b5 100644 --- 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 @@ -1,21 +1,23 @@ 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 +-- 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_ou_id + substring(level_code FROM '^([^ ]+)') AS parent_bi_ou_id FROM observation_unit_level WHERE level_code LIKE '% %' ), 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 external_reference ex ON ouex.external_references_id = ex.id - JOIN levels_and_parent_ou_bi_id ON ex.external_reference_id = levels_and_parent_ou_bi_id.parent_ou_id + JOIN levels_and_parent_ou_bi_id ON ex.external_reference_id = levels_and_parent_ou_bi_id.parent_bi_ou_id ) UPDATE observation_unit_level SET level_code = regexp_replace( From 56c982163159f85b58f3d90b8e82ec43af5c2d49 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Wed, 22 Jul 2026 16:33:19 -0400 Subject: [PATCH 11/56] Migrate sample bi ouIds to brapi ouIds --- .../V006_002__migrate_sample_ou_ids.sql | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/main/resources/db/migration/V006_002__migrate_sample_ou_ids.sql 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..e944c439 --- /dev/null +++ b/src/main/resources/db/migration/V006_002__migrate_sample_ou_ids.sql @@ -0,0 +1,27 @@ +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' + ), + 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 external_reference ex ON ouex.external_references_id = ex.id + JOIN samples_and_bi_ou_ids ON ex.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 \ No newline at end of file From e079626f178592c6c270a8160dde5f23dfd1ec68 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Fri, 24 Jul 2026 12:54:11 -0400 Subject: [PATCH 12/56] Add back in distinct to general search build query - This was added for a previous implementation of filter/sorting additional info data that was required by grouping. This is not needed anymore considering now we are using a jpa/hibernate way to grab this data using the entity name for the fields --- .../test/BrAPITestServer/service/SearchQueryBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 61ca21f8..007249e2 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -23,8 +23,8 @@ public class SearchQueryBuilder { private Class clazz; public SearchQueryBuilder(Class clazz) { - this.selectClause = "SELECT entity FROM " + clazz.getSimpleName() + " entity "; - this.selectOnlyIds = "SELECT entity.id FROM " + clazz.getSimpleName() + " entity "; + this.selectClause = "SELECT distinct entity FROM " + clazz.getSimpleName() + " entity "; + this.selectOnlyIds = "SELECT distinct entity.id FROM " + clazz.getSimpleName() + " entity "; this.whereClause = "WHERE 1=1 "; this.defaultSort = " ORDER BY entity.id ASC "; this.sortClause = ""; From 647b44fad90ed5cea67415290881a2879799c00d Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 27 Jul 2026 17:43:22 -0400 Subject: [PATCH 13/56] Implement data types for filtering - Filtering previously was failing on ID/UUID types because of the use of lower and like on UUID type. Supported column name filtering needed types then to differentiate which sql code would be generated based off of the data type of the column. More data types can be supported as needed --- .../java/io/swagger/model/SearchRequest.java | 30 ++++++++-------- .../model/core/StudySearchRequest.java | 30 ++++++++-------- .../model/core/TrialSearchRequest.java | 24 +++++++------ .../model/dto/EntityColumnNameAndType.java | 28 +++++++++++++++ .../BrAPITestServer/model/dto/EntityType.java | 8 +++++ .../service/SearchQueryBuilder.java | 35 ++++++++++++++++--- .../service/core/StudyService.java | 2 +- .../service/core/TrialService.java | 4 +-- 8 files changed, 113 insertions(+), 48 deletions(-) create mode 100644 src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityColumnNameAndType.java create mode 100644 src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityType.java diff --git a/src/main/java/io/swagger/model/SearchRequest.java b/src/main/java/io/swagger/model/SearchRequest.java index 828cd69c..aaad3b9a 100644 --- a/src/main/java/io/swagger/model/SearchRequest.java +++ b/src/main/java/io/swagger/model/SearchRequest.java @@ -2,9 +2,10 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; 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; @@ -149,7 +150,7 @@ public void setFilterBy(List filterBy) throws BrAPIServerException { return; } - Map allowedSortFilterNames = getSortFilterEntityColumnNamesByRequestName(); + Map allowedSortFilterNames = getEntityColAndTypeBySubmittedNameMap(); for (FilterBy filterByItem : filterBy) { @@ -161,15 +162,17 @@ public void setFilterBy(List filterBy) throws BrAPIServerException { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "value attribute not provided in element of filterBy list."); } - String filterColumnEntityName = getSortFilterEntityColumnNamesByRequestName().get(filterByItem.getFilterOn()); + EntityColumnNameAndType entityColumnNameAndType = allowedSortFilterNames.get(filterByItem.getFilterOn()); - if (filterColumnEntityName == null) { + if (entityColumnNameAndType == null) { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, String.format("Supplied filterColumn [%s] not available in allowed names [%s]", filterByItem.getFilterOn(), allowedSortFilterNames.keySet()) ); - } else { - // Remap suppliedFilterColumn to actual entity name supplied by mapper - filterByItem.setFilterOn(filterColumnEntityName); + } + + 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; @@ -185,29 +188,26 @@ public void setSortBy(List sortBy) throws BrAPIServerException { return; } - Map allowedSortFilterNames = getSortFilterEntityColumnNamesByRequestName(); + 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"); } - String sortColumnEntityName = getSortFilterEntityColumnNamesByRequestName().get(sortByItem.getSortedOn()); + EntityColumnNameAndType sortColumnEntityNameAndType = getEntityColAndTypeBySubmittedNameMap().get(sortByItem.getSortedOn()); - if (sortColumnEntityName == null) { + if (sortColumnEntityNameAndType == null) { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, String.format("Supplied sortColumn [%s] not available in allowed names [%s]", sortByItem.getSortedOn(), allowedSortFilterNames.keySet()) ); - } else { - // Remap suppliedFilterColumn to actual entity name supplied by mapper - sortByItem.setSortedOn(sortColumnEntityName); } } this.sortBy = sortBy; } - public Map getSortFilterEntityColumnNamesByRequestName() { - throw new UnsupportedOperationException(String.format("Sort/Filtering not implemented for %s", this.getClass().getSimpleName())); + 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/StudySearchRequest.java b/src/main/java/io/swagger/model/core/StudySearchRequest.java index 78ed90d0..94e760ff 100644 --- a/src/main/java/io/swagger/model/core/StudySearchRequest.java +++ b/src/main/java/io/swagger/model/core/StudySearchRequest.java @@ -6,6 +6,8 @@ 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; @@ -13,20 +15,20 @@ public class StudySearchRequest extends SearchRequest { // Key - allowed sort or field filter name for this entity - // Value = entity field name that represents the submitted field. Used later on in query building. - private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = + // 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", "*obsunit.germplasm.id"), - Map.entry("locationDbId", "location.id"), - Map.entry("observationVariableDbId", "*observation.observationVariable.id"), - Map.entry("programDbId", "trial.program.id"), - Map.entry("programName", "trial.program.name"), - Map.entry("seasonDbId", "*season.id"), - Map.entry("studyDbId", "id"), - Map.entry("studyLocation", "location.id"), - Map.entry("trialDbId", "trial.id"), - Map.entry("studyType", "studyName"), - Map.entry("studyName", "studyName") + 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") @@ -579,7 +581,7 @@ public Integer getTotalParameterCount() { } @Override - public Map getSortFilterEntityColumnNamesByRequestName() { + 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 ce24b1f7..a4a2feaf 100644 --- a/src/main/java/io/swagger/model/core/TrialSearchRequest.java +++ b/src/main/java/io/swagger/model/core/TrialSearchRequest.java @@ -4,23 +4,25 @@ 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.time.LocalDate; public class TrialSearchRequest extends SearchRequest { // Key - allowed sort or field filter name for this entity - // Value = entity field name that represents the submitted field. Used later on in query building. - private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = + // 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", "trialName", - "createdDate", "createdDate", - "createdBy", "createdBy", - "trialDbId", "id", - "programDbId","program.id", - "startDate", "startDate", - "endDate", "endDate", - "active", "active" + "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) ); @JsonProperty("commonCropNames") @@ -440,7 +442,7 @@ public Integer getTotalParameterCount() { } @Override - public Map getSortFilterEntityColumnNamesByRequestName() { + public Map getEntityColAndTypeBySubmittedNameMap() { return ALLOWED_SORT_AND_FILTER_FIELDS; } } 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/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index 007249e2..c7e7da92 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -11,6 +11,8 @@ import io.swagger.model.sort.SortBy; import io.swagger.model.sort.SortOrder; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; +import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; +import org.brapi.test.BrAPITestServer.model.dto.EntityType; public class SearchQueryBuilder { @@ -146,6 +148,16 @@ public SearchQueryBuilder appendLike(String like, String columnName) { return this; } + public SearchQueryBuilder appendLikeIDs(String like, String columnName) { + String paramName = paramFilter(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) { @@ -329,20 +341,24 @@ private String paramFilter(String param) { } /** - * Takes a list of SortBy options that should typically come in a searchRequest. + * 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) throws BrAPIServerException { + public SearchQueryBuilder sortBy(List sortBy, Map entityColAndTypeBySubmittedName) throws BrAPIServerException { if (sortBy == null || sortBy.isEmpty()) { return this; } for (SortBy sort : sortBy) { + // At this point, the submitted sortBy name has been verified to be in entityColAndTypeBySubmittedName + sort.setSortedOn(entityColAndTypeBySubmittedName.get(sort.getSortedOn()).getEntityColumnName()); + if (sortBy.getFirst().equals(sort)) { this.sortClause += " ORDER BY "; buildSort(sort); @@ -360,13 +376,15 @@ private void buildSort(SortBy sort) { } /** - * Takes a list of FilterBy options that should typically come in a searchRequest. + * 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) throws BrAPIServerException { + public SearchQueryBuilder filterBy(List filterBy, Map entityColAndTypeBySubmittedName) throws BrAPIServerException { SearchQueryBuilder searchQuery = this; if (filterBy == null || filterBy.isEmpty()) { @@ -374,7 +392,14 @@ public SearchQueryBuilder filterBy(List filterBy) throws BrAPIServe } for (FilterBy filter : filterBy) { - searchQuery = appendLike(filter.getValue().toLowerCase(), filter.getFilterOn()); + // At this point, the submitted filterBy column name has been verified to be in entityColAndTypeBySubmittedName + EntityColumnNameAndType entityColumnNameAndType = entityColAndTypeBySubmittedName.get(filter.getFilterOn()); + + 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; 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 e5611ab7..602234a2 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 @@ -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") - .sortBy(request.getSortByElements()); + .sortBy(request.getSortByElements(), request.getEntityColAndTypeBySubmittedNameMap()); Page studiesPage = studyRepository.findAllBySearchAndPaginate(searchQuery, pageReq); PagingUtility.calculateMetaData(metaData, studiesPage); 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 21146a30..9aa723a9 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 @@ -127,8 +127,8 @@ public List findTrials(@Valid TrialSearchRequest request, Metadata metada .appendList(request.getStudyNames(), "*study.studyName").appendList(request.getTrialDbIds(), "id") .appendList(request.getTrialNames(), "trialName") .appendDateRange(request.getSearchDateRangeStart(), request.getSearchDateRangeEnd(), "startDate") - .sortBy(request.getSortByElements()) - .filterBy(request.getFilterBy()); + .sortBy(request.getSortByElements(), request.getEntityColAndTypeBySubmittedNameMap()) + .filterBy(request.getFilterBy(), request.getEntityColAndTypeBySubmittedNameMap()); Page trialsPage = trialRepository.findAllBySearchAndPaginate(searchQuery, pageReq); PagingUtility.calculateMetaData(metadata, trialsPage); From b095c97517c11ca0a1758c9cd5a87eda0e7a619d Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 3 Aug 2026 14:37:43 -0400 Subject: [PATCH 14/56] Add code for filtering on one to many relationship entity collections --- .../model/core/TrialSearchRequest.java | 4 +- .../service/SearchQueryBuilder.java | 37 +++++++++++++++---- .../service/core/TrialService.java | 10 ++--- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/main/java/io/swagger/model/core/TrialSearchRequest.java b/src/main/java/io/swagger/model/core/TrialSearchRequest.java index a4a2feaf..2760e5d5 100644 --- a/src/main/java/io/swagger/model/core/TrialSearchRequest.java +++ b/src/main/java/io/swagger/model/core/TrialSearchRequest.java @@ -22,7 +22,9 @@ public class TrialSearchRequest extends SearchRequest { "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) + "active", new EntityColumnNameAndType("active", EntityType.BOOLEAN), + "programName", new EntityColumnNameAndType("program.name", EntityType.TEXT), + "locationDbId", new EntityColumnNameAndType("*studies.location.id", EntityType.UUID) ); @JsonProperty("commonCropNames") 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 c7e7da92..e93310bc 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -9,10 +9,10 @@ import io.swagger.model.FilterBy; import io.swagger.model.GeoJSONSearchArea; import io.swagger.model.sort.SortBy; -import io.swagger.model.sort.SortOrder; 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 { @@ -330,10 +330,6 @@ private String entityPrefix(String field) { } } - private String addInfoPrefix(String field) { - return "function('jsonb_extract_path_text', entity.additionalInfo, '" + field + "' ) "; - } - private String paramFilter(String param) { if (param == null) return ""; @@ -357,7 +353,15 @@ public SearchQueryBuilder sortBy(List sortBy, Map filterBy(List filterBy, Map filterBy(List filterBy, Map 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") .sortBy(request.getSortByElements(), request.getEntityColAndTypeBySubmittedNameMap()) From 05fb0822f6b9174e451231b16a9fda59374b3645 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 4 Aug 2026 14:28:31 -0400 Subject: [PATCH 15/56] Fix issue with sorting for GET study/trial endpoints --- src/main/java/io/swagger/model/core/StudySearchRequest.java | 4 ++-- src/main/java/io/swagger/model/sort/SortOrder.java | 2 +- .../test/BrAPITestServer/service/SearchQueryBuilder.java | 2 -- .../brapi/test/BrAPITestServer/service/core/StudyService.java | 2 +- .../brapi/test/BrAPITestServer/service/core/TrialService.java | 2 +- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/swagger/model/core/StudySearchRequest.java b/src/main/java/io/swagger/model/core/StudySearchRequest.java index 94e760ff..26a134f0 100644 --- a/src/main/java/io/swagger/model/core/StudySearchRequest.java +++ b/src/main/java/io/swagger/model/core/StudySearchRequest.java @@ -21,8 +21,8 @@ public class StudySearchRequest extends SearchRequest { 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("programDbId", new EntityColumnNameAndType("program.id", EntityType.UUID)), + Map.entry("programName", new EntityColumnNameAndType("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)), diff --git a/src/main/java/io/swagger/model/sort/SortOrder.java b/src/main/java/io/swagger/model/sort/SortOrder.java index 4cc9d5aa..b8a85ebf 100644 --- a/src/main/java/io/swagger/model/sort/SortOrder.java +++ b/src/main/java/io/swagger/model/sort/SortOrder.java @@ -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/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index e93310bc..f13179e7 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -361,8 +361,6 @@ public SearchQueryBuilder sortBy(List sortBy, Map findStudies(String commonCropName, String studyType, String p if (active != null) request.setActive(active); if (sortBy != null) { - SortBy sortByElement = new SortBy(sortBy, SortOrder.valueOf(sortOrder)); + SortBy sortByElement = new SortBy(sortBy, SortOrder.fromValue(sortOrder)); request.setSortBy(List.of(sortByElement)); } 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 601c4606..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 @@ -97,7 +97,7 @@ public List findTrials(@Valid String commonCropName, @Valid String contac if (searchDateRangeEnd != null) request.setSearchDateRangeEnd(searchDateRangeEnd); if (sortBy != null) { - SortBy querySortBy = new SortBy(sortBy, SortOrder.valueOf(sortOrder)); + SortBy querySortBy = new SortBy(sortBy, SortOrder.fromValue(sortOrder)); request.setSortBy(List.of(querySortBy)); } request.addExternalReferenceItem(externalReferenceId, externalReferenceID, externalReferenceSource); From b0bf8baaafeadacaa0b49bf21238b64fb15e1bae Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Thu, 6 Aug 2026 18:15:36 -0400 Subject: [PATCH 16/56] Fix issues with using distinct in sorting --- .../model/core/StudySearchRequest.java | 4 +- .../service/SearchQueryBuilder.java | 52 +++++++++++++++---- .../service/germ/GermplasmService.java | 1 + 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/main/java/io/swagger/model/core/StudySearchRequest.java b/src/main/java/io/swagger/model/core/StudySearchRequest.java index 26a134f0..94e760ff 100644 --- a/src/main/java/io/swagger/model/core/StudySearchRequest.java +++ b/src/main/java/io/swagger/model/core/StudySearchRequest.java @@ -21,8 +21,8 @@ public class StudySearchRequest extends SearchRequest { 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("program.id", EntityType.UUID)), - Map.entry("programName", new EntityColumnNameAndType("program.name", EntityType.TEXT)), + 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)), 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 f13179e7..cd9e3020 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -22,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) { @@ -283,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; } @@ -305,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; } @@ -315,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; } @@ -355,11 +379,24 @@ public SearchQueryBuilder sortBy(List sortBy, Map 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 "; @@ -419,9 +456,6 @@ public SearchQueryBuilder filterBy(List filterBy, Map 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); From 22736ae07cb6a444569455be53d42637591e5ca6 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Fri, 7 Aug 2026 12:14:29 -0400 Subject: [PATCH 17/56] Add Pattern to end of filter params in SearchQueryBuilder - This prevents collisions with existing parameter lookups --- .../BrAPITestServer/service/SearchQueryBuilder.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 cd9e3020..d18a30a0 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -141,7 +141,7 @@ public SearchQueryBuilder appendSingle(UUID single, String columnName) { } public SearchQueryBuilder appendLike(String like, String columnName) { - String paramName = paramFilter(columnName); + String paramName = paramFilterPattern(columnName); if (like != null) { this.whereClause += "AND lower(" + entityPrefix(columnName) + ") LIKE :" + paramName + " "; @@ -151,7 +151,7 @@ public SearchQueryBuilder appendLike(String like, String columnName) { } public SearchQueryBuilder appendLikeIDs(String like, String columnName) { - String paramName = paramFilter(columnName); + String paramName = paramFilterPattern(columnName); if (like != null) { this.whereClause += "AND cast(" + entityPrefix(columnName) + " as String) LIKE :" + paramName + " "; @@ -360,6 +360,12 @@ private String paramFilter(String param) { return param.replace('.', '_').replace('*', '_'); } + 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. From 2bd1d4799929554c84d7625697deb36c9c90434a Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 10 Aug 2026 17:35:13 -0400 Subject: [PATCH 18/56] Create migration script, add programDbId to entity and model --- .../model/germ/GermplasmNewRequest.java | 17 ++++++++++++-- .../model/entity/germ/GermplasmEntity.java | 11 ++++++++++ ...dd_program_id_to_germplasm_and_migrate.sql | 22 +++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 src/main/resources/db/migration/V006_003__add_program_id_to_germplasm_and_migrate.sql diff --git a/src/main/java/io/swagger/model/germ/GermplasmNewRequest.java b/src/main/java/io/swagger/model/germ/GermplasmNewRequest.java index 253783e7..b52c5f09 100644 --- a/src/main/java/io/swagger/model/germ/GermplasmNewRequest.java +++ b/src/main/java/io/swagger/model/germ/GermplasmNewRequest.java @@ -97,6 +97,9 @@ public class GermplasmNewRequest extends BrAPIDataModel { @JsonProperty("taxonIds") private List taxonIds = null; + @JsonProperty("programDbId") + private String programDbId = null; + public String getBreedingMethodName() { return breedingMethodName; } @@ -510,6 +513,14 @@ public void setTaxonIds(List taxonIds) { this.taxonIds = taxonIds; } + public String getProgramDbId() { + return programDbId; + } + + public void setProgramDbId(String programDbId) { + this.programDbId = programDbId; + } + @Override public boolean equals(java.lang.Object o) { if (this == o) { @@ -550,7 +561,8 @@ 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); } @Override @@ -560,7 +572,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); } @Override @@ -600,6 +612,7 @@ 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("}"); return sb.toString(); } 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/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..9f5b2aae --- /dev/null +++ b/src/main/resources/db/migration/V006_003__add_program_id_to_germplasm_and_migrate.sql @@ -0,0 +1,22 @@ +ALTER TABLE germplasm +ADD COLUMN program_id UUID; + +ALTER TABLE germplasm +ADD CONSTRAINT germplasm_program_fk + FOREIGN KEY (program_id) + REFERENCES public.program(id) + ON DELETE CASCADE; + +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 +) pquery +where id = pquery.germplasm_id; \ No newline at end of file From 2bad4ff95942ca919f86eaae2406354455798bbe Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 11 Aug 2026 14:52:38 -0400 Subject: [PATCH 19/56] Support for programId in GermplasmApi --- .../io/swagger/api/germ/GermplasmApi.java | 1 + .../model/germ/GermplasmNewRequest.java | 17 +++++++- .../germ/GermplasmApiController.java | 3 +- .../service/core/ProgramService.java | 31 ++++++++++---- .../service/germ/GermplasmService.java | 42 ++++++++++++++++--- 5 files changed, 78 insertions(+), 16 deletions(-) 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/germ/GermplasmNewRequest.java b/src/main/java/io/swagger/model/germ/GermplasmNewRequest.java index b52c5f09..7317f374 100644 --- a/src/main/java/io/swagger/model/germ/GermplasmNewRequest.java +++ b/src/main/java/io/swagger/model/germ/GermplasmNewRequest.java @@ -100,6 +100,9 @@ public class GermplasmNewRequest extends BrAPIDataModel { @JsonProperty("programDbId") private String programDbId = null; + @JsonProperty("programName") + private String programName = null; + public String getBreedingMethodName() { return breedingMethodName; } @@ -521,6 +524,14 @@ 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) { @@ -562,7 +573,8 @@ public boolean equals(java.lang.Object o) { && Objects.equals(this.subtaxaAuthority, germplasmNewRequest.subtaxaAuthority) && Objects.equals(this.synonyms, germplasmNewRequest.synonyms) && Objects.equals(this.taxonIds, germplasmNewRequest.taxonIds) - && Objects.equals(this.programDbId, germplasmNewRequest.programDbId); + && Objects.equals(this.programDbId, germplasmNewRequest.programDbId) + && Objects.equals(this.programName, germplasmNewRequest.programName); } @Override @@ -572,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, programDbId); + subtaxaAuthority, synonyms, taxonIds, programDbId, programName); } @Override @@ -613,6 +625,7 @@ public String toString() { 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/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/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/germ/GermplasmService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java index 2946fd58..a09abdad 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); @@ -171,12 +178,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 +202,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 +534,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 +593,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 +607,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 +626,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 +683,10 @@ private List createEntitiesInBatch(List bo updateSynonymEntities(request.getSynonyms(), entity); if (request.getTaxonIds() != null) updateTaxonEntities(request.getTaxonIds(), entity); + if (!foundProgramsByDbId.isEmpty()) { + ProgramEntity program = foundProgramsByDbId.get(UUID.fromString(request.getProgramDbId())); + entity.setProgram(program); + } toSave.add(entity); } @@ -738,6 +764,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) { From 5a747b473dcc651ace2acc823f88f620b989b168 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 18 Aug 2026 16:20:13 -0400 Subject: [PATCH 20/56] Use more restrictive exref query, add assertion to migration --- .../V006_001__migrate_parent_ou_ids.sql | 53 ++++++++++++++++--- 1 file changed, 45 insertions(+), 8 deletions(-) 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 index 3dd227b5..1cb2c600 100644 --- 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 @@ -9,15 +9,26 @@ WITH 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 external_reference ex ON ouex.external_references_id = ex.id - JOIN levels_and_parent_ou_bi_id ON ex.external_reference_id = levels_and_parent_ou_bi_id.parent_bi_ou_id +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( @@ -26,4 +37,30 @@ SET level_code = regexp_replace( mol.ou_id::text ) FROM ou_ids_matched_on_levels mol -WHERE observation_unit_level.id = mol.level_id \ No newline at end of file +WHERE observation_unit_level.id = mol.level_id + +-- Assertion to assure we don't remove additional_info.observationLevel where +-- references can't be updated. For our prod data, shouldn't be any. +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 From c17b97a3fe74b95475522d07097f347869472342 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 18 Aug 2026 16:42:05 -0400 Subject: [PATCH 21/56] Add semicolon --- .../resources/db/migration/V006_001__migrate_parent_ou_ids.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 1cb2c600..a6413c46 100644 --- 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 @@ -37,7 +37,7 @@ SET level_code = regexp_replace( mol.ou_id::text ) FROM ou_ids_matched_on_levels mol -WHERE observation_unit_level.id = mol.level_id +WHERE observation_unit_level.id = mol.level_id; -- Assertion to assure we don't remove additional_info.observationLevel where -- references can't be updated. For our prod data, shouldn't be any. From cc05724e809243ad6eb1d187149b2d9f8ed02775 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 18 Aug 2026 20:25:51 -0400 Subject: [PATCH 22/56] Update comment --- .../resources/db/migration/V006_001__migrate_parent_ou_ids.sql | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 index a6413c46..2166b229 100644 --- 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 @@ -39,8 +39,7 @@ SET level_code = regexp_replace( FROM ou_ids_matched_on_levels mol WHERE observation_unit_level.id = mol.level_id; --- Assertion to assure we don't remove additional_info.observationLevel where --- references can't be updated. For our prod data, shouldn't be any. +-- 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; From cc82ecdc4458c1dfbd1238f1bc02e82ebc50711b Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 18 Aug 2026 20:46:48 -0400 Subject: [PATCH 23/56] Update migration script with stricter matching to OU, add assertion --- .../V006_002__migrate_sample_ou_ids.sql | 58 +++++++++++++++---- 1 file changed, 46 insertions(+), 12 deletions(-) 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 index e944c439..5ff206d3 100644 --- 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 @@ -4,24 +4,58 @@ WITH SELECT id as sample_id, additional_info ->> 'obsUnitID' AS bi_ou_id -FROM sample -WHERE additional_info ? 'obsUnitID' + 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 external_reference ex ON ouex.external_references_id = ex.id - JOIN samples_and_bi_ou_ids ON ex.external_reference_id = samples_and_bi_ou_ids.bi_ou_id + 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 \ No newline at end of file +) +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 From b30833000468b5ef8d0b31bae7cb43a9d0db7e41 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Thu, 20 Aug 2026 16:31:19 -0400 Subject: [PATCH 24/56] Undo removed changes from develop --- pom.xml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a1e201c5..e5017b16 100644 --- a/pom.xml +++ b/pom.xml @@ -152,7 +152,7 @@ org.springframework.boot spring-boot-maven-plugin - ${spring.version} + 2.7.5 @@ -164,4 +164,21 @@ + + + spring-releases + https://repo.spring.io/libs-release + + + spring-milestone + https://repo.spring.io/libs-milestone + + + + + spring-releases + https://repo.spring.io/libs-release + + + From 265da1423c9e074c6ac6c003ec852274e8a7ede6 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 24 Aug 2026 12:38:44 -0400 Subject: [PATCH 25/56] Ignore getMethod on sortFilter map that was breaking async use case --- src/main/java/io/swagger/model/SearchRequest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/io/swagger/model/SearchRequest.java b/src/main/java/io/swagger/model/SearchRequest.java index aaad3b9a..b59090fe 100644 --- a/src/main/java/io/swagger/model/SearchRequest.java +++ b/src/main/java/io/swagger/model/SearchRequest.java @@ -207,6 +207,7 @@ public void setSortBy(List sortBy) throws BrAPIServerException { 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())); } From 5a39677a17cabe8ee247fd4649369efd362ec069 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Wed, 26 Aug 2026 13:39:08 -0400 Subject: [PATCH 26/56] Fix createEntities in batch error, address migration concerns --- .../service/germ/GermplasmService.java | 2 +- ...dd_program_id_to_germplasm_and_migrate.sql | 28 +++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) 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 2d36e506..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 @@ -684,7 +684,7 @@ private List createEntitiesInBatch(List bo updateSynonymEntities(request.getSynonyms(), entity); if (request.getTaxonIds() != null) updateTaxonEntities(request.getTaxonIds(), entity); - if (!foundProgramsByDbId.isEmpty()) { + if (request.getProgramDbId() != null) { ProgramEntity program = foundProgramsByDbId.get(UUID.fromString(request.getProgramDbId())); entity.setProgram(program); } 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 index 9f5b2aae..eb13b87a 100644 --- 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 @@ -4,19 +4,37 @@ ADD COLUMN program_id UUID; ALTER TABLE germplasm ADD CONSTRAINT germplasm_program_fk FOREIGN KEY (program_id) - REFERENCES public.program(id) - ON DELETE CASCADE; + 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 + 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 -) pquery -where id = pquery.germplasm_id; \ No newline at end of file + 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 From 1ec93634b1cf8547b3102084564a4ff424d2ce56 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 31 Aug 2026 12:20:14 -0400 Subject: [PATCH 27/56] Remove dbIds from paramCount for Germplasm --- src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java b/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java index 36c58878..79bbf045 100644 --- a/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java +++ b/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java @@ -592,8 +592,6 @@ public Integer getTotalParameterCount() { count += this.familyCodes.size(); if (this.genus != null) count += this.genus.size(); - if (this.germplasmDbIds != null) - count += this.germplasmDbIds.size(); if (this.germplasmNames != null) count += this.germplasmNames.size(); if (this.germplasmPUIs != null) From f6033fd90fceb04c2698af3f2b45633d4bfece36 Mon Sep 17 00:00:00 2001 From: humsika Date: Tue, 22 Sep 2026 10:56:04 -0500 Subject: [PATCH 28/56] BI-2922: Added the filter parms. --- .../java/io/swagger/model/core/StudySearchRequest.java | 7 ++++++- .../test/BrAPITestServer/service/core/StudyService.java | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/swagger/model/core/StudySearchRequest.java b/src/main/java/io/swagger/model/core/StudySearchRequest.java index 94e760ff..ce9b0376 100644 --- a/src/main/java/io/swagger/model/core/StudySearchRequest.java +++ b/src/main/java/io/swagger/model/core/StudySearchRequest.java @@ -18,16 +18,21 @@ public class StudySearchRequest extends SearchRequest { // 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("commonCropName", new EntityColumnNameAndType("crop.cropName", EntityType.TEXT)), + Map.entry("externalReferenceId", new EntityColumnNameAndType("*externalReferences.externalReferenceId", EntityType.TEXT)), + Map.entry("externalReferenceSource", new EntityColumnNameAndType("*externalReferences.externalReferenceSource", EntityType.TEXT)), 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("studyCode", new EntityColumnNameAndType("studyCode", EntityType.TEXT)), Map.entry("studyDbId", new EntityColumnNameAndType("id", EntityType.UUID)), Map.entry("studyLocation", new EntityColumnNameAndType("location.id", EntityType.UUID)), + Map.entry("studyPUI", new EntityColumnNameAndType("studyPUI", EntityType.TEXT)), Map.entry("trialDbId", new EntityColumnNameAndType("trial.id", EntityType.UUID)), - Map.entry("studyType", new EntityColumnNameAndType("studyName", EntityType.TEXT)), + Map.entry("studyType", new EntityColumnNameAndType("studyType", EntityType.TEXT)), Map.entry("studyName", new EntityColumnNameAndType("studyName", EntityType.TEXT)) ); 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 ae6a297b..096e9cf7 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 @@ -159,7 +159,8 @@ 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") - .sortBy(request.getSortByElements(), request.getEntityColAndTypeBySubmittedNameMap()); + .sortBy(request.getSortByElements(), request.getEntityColAndTypeBySubmittedNameMap()) + .filterBy(request.getFilterBy(), request.getEntityColAndTypeBySubmittedNameMap()); Page studiesPage = studyRepository.findAllBySearchAndPaginate(searchQuery, pageReq); PagingUtility.calculateMetaData(metaData, studiesPage); From 5503404c76f8a96532bbb06fea7c22acf0d80a14 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Wed, 16 Sep 2026 19:01:41 -0400 Subject: [PATCH 29/56] Changes to support filtering/sorting for germplasm - Additional changes made to support sorting by numbers, especially for additionalInfo fields - Bugs fixed when left join fetches were made but filters were required for specific fields --- .../model/germ/GermplasmSearchRequest.java | 27 +++++++++++++++++++ .../BrAPITestServer/model/dto/EntityType.java | 4 ++- .../model/entity/germ/GermplasmEntity.java | 19 +++++++++++++ .../service/SearchQueryBuilder.java | 22 ++++++++------- .../service/germ/GermplasmService.java | 13 +++++---- 5 files changed, 70 insertions(+), 15 deletions(-) diff --git a/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java b/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java index 79bbf045..d1acd966 100644 --- a/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java +++ b/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java @@ -3,12 +3,34 @@ import com.fasterxml.jackson.annotation.JsonIgnore; 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; +import java.util.Map; import java.util.Objects; public class GermplasmSearchRequest 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( + // accessionNumber = GID for DeltaBreed + Map.entry("accessionNumber", new EntityColumnNameAndType("accessionNumber", EntityType.NUMBER)), + Map.entry("defaultDisplayName", new EntityColumnNameAndType("defaultDisplayName", EntityType.TEXT)), + Map.entry("breedingMethod", new EntityColumnNameAndType("methodName", EntityType.TEXT)), + Map.entry("seedSource", new EntityColumnNameAndType("seedSource", EntityType.TEXT)), + Map.entry("pedigree",new EntityColumnNameAndType("pedigree.pedigreeString", EntityType.TEXT)), + Map.entry("femaleParentGID", new EntityColumnNameAndType("femaleParentGid", EntityType.NUMBER)), + Map.entry("maleParentGID", new EntityColumnNameAndType("maleParentGid", EntityType.NUMBER)), + Map.entry("createdDate", new EntityColumnNameAndType("createdDate", EntityType.TEXT)), + Map.entry("createdBy", new EntityColumnNameAndType("createdBy", EntityType.TEXT)), + Map.entry("importEntryNumber", new EntityColumnNameAndType("importEntryNumber", EntityType.NUMBER)), + Map.entry("synonyms", new EntityColumnNameAndType("*synonyms.synonym", EntityType.TEXT)) + ); + @JsonProperty("accessionNumbers") private List accessionNumbers = null; @@ -621,4 +643,9 @@ public Integer getTotalParameterCount() { return count; } + + @Override + public Map getEntityColAndTypeBySubmittedNameMap() { + return ALLOWED_SORT_AND_FILTER_FIELDS; + } } 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 index e69aa988..8f1156a8 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityType.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityType.java @@ -3,6 +3,8 @@ public enum EntityType { TEXT, UUID, - BOOLEAN + BOOLEAN, + // Sorting is done at the database schema/formula level for numbers. See GermplasmEntity.maleParentGid. No changes to filtering required for this datatype. + NUMBER // 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/germ/GermplasmEntity.java b/src/main/java/org/brapi/test/BrAPITestServer/model/entity/germ/GermplasmEntity.java index 8b464b85..821c2873 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 @@ -15,6 +15,7 @@ import org.brapi.test.BrAPITestServer.model.entity.pheno.TaxonEntity; import org.hibernate.annotations.BatchSize; +import org.hibernate.annotations.Formula; import org.hibernate.annotations.Where; import java.util.ArrayList; @@ -100,6 +101,24 @@ public class GermplasmEntity extends BrAPIPrimaryEntity { @ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY) private ProgramEntity program; + @Formula("(additional_info #>> '{createdDate}')") + private String createdDate; + + @Formula("(additional_info #>> '{createdBy,userName}')") + private String createdBy; + + @Formula("(additional_info #>> '{breedingMethod}')") + private String methodName; + + @Formula("cast(additional_info #>> '{femaleParentGid}' AS INTEGER)") + private Integer femaleParentGid; + + @Formula("cast(additional_info #>> '{maleParentGid}' AS INTEGER)") + private Integer maleParentGid; + + @Formula("cast(additional_info #>> '{importEntryNumber}' AS INTEGER)") + private Integer importEntryNumber; + public GermplasmInstituteEntity getHostInstitute() { if (getInstitutes() != null) { for (GermplasmInstituteEntity institute : getInstitutes()) { 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 d18a30a0..d70faf7c 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -46,12 +46,7 @@ public String getQuery() { } public String getIdQuery() { - if (sortClause.isEmpty()) { - // By default, sort on entity id to have query result remain idempotent - sortClause = defaultSort; - } - - return selectOnlyIds + whereClause + sortClause; + return selectOnlyIds + whereClause; } public Map getParams() { @@ -150,7 +145,8 @@ public SearchQueryBuilder appendLike(String like, String columnName) { return this; } - public SearchQueryBuilder appendLikeIDs(String like, String columnName) { + // Used to convert non-string fields to string and use a like filter comparison + public SearchQueryBuilder appendLikeString(String like, String columnName) { String paramName = paramFilterPattern(columnName); if (like != null) { @@ -290,6 +286,8 @@ public SearchQueryBuilder join(String join, String name) { this.selectClause += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; this.selectOnlyIds += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; this.joinedTables.add(join); + } else if (joinedFetchedTables.contains(join) && !this.joinedTables.contains(join)) { + this.selectOnlyIds += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; } return this; } @@ -416,6 +414,12 @@ public SearchQueryBuilder sortBy(List sortBy, Map resetSortClause() { + this.sortClause = ""; + return this; + } + private void buildSort(SortBy sort) { this.sortClause += entityPrefix(sort.getSortedOn()) + " " + sort.getSortOrder() + " "; } @@ -446,8 +450,8 @@ public SearchQueryBuilder filterBy(List filterBy, Map findGermplasm(@Valid GermplasmSearchRequest request, Meta return germplasms; } - public List findGermplasmWithoutPaging(@Valid GermplasmSearchRequest request) { + public List findGermplasmWithoutPaging(@Valid GermplasmSearchRequest request) throws BrAPIServerException { List entities = findGermplasmEntitiesWithoutPaging(request); return entities.stream().map(this::convertFromEntity).collect(Collectors.toList()); } @@ -159,7 +159,7 @@ public Page findGermplasmEntities(@Valid GermplasmSearchRequest } // 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) { + public List findGermplasmEntitiesWithoutPaging(@Valid GermplasmSearchRequest request) throws BrAPIServerException { SearchQueryBuilder searchQuery = buildGermplasmSearchQuery(request); List germs = germplasmRepository.findAllBySearch(searchQuery); @@ -170,7 +170,7 @@ public List findGermplasmEntitiesWithoutPaging(@Valid Germplasm return germs; } - private SearchQueryBuilder buildGermplasmSearchQuery(GermplasmSearchRequest request) { + private SearchQueryBuilder buildGermplasmSearchQuery(GermplasmSearchRequest request) throws BrAPIServerException { SearchQueryBuilder searchQuery = new SearchQueryBuilder( GermplasmEntity.class); searchQuery.leftJoinFetch("synonyms", "synonyms") @@ -207,7 +207,9 @@ private SearchQueryBuilder buildGermplasmSearchQuery(GermplasmS // .appendList(request.getProgenyDbIds(), "*progeny.germplasmDbId") .appendList(request.getGenus(), "genus").appendList(request.getSpecies(), "species") .appendNamesList(request.getBinomialNames(), "genus", "genus", "species") - .appendList(request.getFamilyCodes(), "familyCode"); + .appendList(request.getFamilyCodes(), "familyCode") + .filterBy(request.getFilterBy(), request.getEntityColAndTypeBySubmittedNameMap()) + .sortBy(request.getSortByElements(), request.getEntityColAndTypeBySubmittedNameMap()); return searchQuery; } @@ -339,7 +341,8 @@ private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder Date: Mon, 28 Sep 2026 17:34:09 -0400 Subject: [PATCH 30/56] Address issues with removing sorting from getIdQuery --- .../service/SearchQueryBuilder.java | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) 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 d70faf7c..ad6f4a36 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -18,6 +18,7 @@ public class SearchQueryBuilder { private String selectClause; private String selectOnlyIds; + private String selectOnlyIdsJoinsAndFilters; private String whereClause; private String defaultSort; private String sortClause; @@ -28,7 +29,8 @@ public class SearchQueryBuilder { public SearchQueryBuilder(Class clazz) { this.selectClause = "SELECT distinct entity FROM " + clazz.getSimpleName() + " entity "; - this.selectOnlyIds = "SELECT distinct entity.id FROM " + clazz.getSimpleName() + " entity "; + this.selectOnlyIds = "SELECT entity.id FROM " + clazz.getSimpleName() + " entity "; + this.selectOnlyIdsJoinsAndFilters = "SELECT distinct entity.id FROM " + clazz.getSimpleName() + " entity2 "; this.whereClause = "WHERE 1=1 "; this.defaultSort = " ORDER BY entity.id ASC "; this.sortClause = ""; @@ -45,8 +47,34 @@ public String getQuery() { return selectClause + whereClause + sortClause; } + /** + * The goal of this query is to return a query that will return only the BrAPI dbIds of the source entity, applying + * sorting, filtering, and pagination to the query. + * + * This has to be done in a very deliberate way because of both HQL and postgres constraints, so this query should + * end up looking something like this: + * + * SELECT entity.id + * FROM GermplasmEntity entity + * WHERE entity.id IN ( + * SELECT DISTINCT entity2.id + * FROM GermplasmEntity entity2 + * JOIN ... + * WHERE ... + * ) + * ORDER BY entity.germplasmName + * + * By containing the filtering and the joining in a subquery with a DISTINCT, we can control potential duplicates from breaking through the cracks. + * Then we can apply sorting outside of this query without a distinct to avoid postgres's requirement to include every order by column in the select clause. + * Distinct is not an issue here, since we are selecting only on id, which is always unique. + */ public String getIdQuery() { - return selectOnlyIds + whereClause; + if (sortClause.isEmpty()) { + // By default, sort on entity id to have query result remain idempotent + sortClause = defaultSort; + } + + return selectOnlyIds + " WHERE entity.id IN (" + selectOnlyIdsJoinsAndFilters + whereClause + ") " + sortClause; } public Map getParams() { @@ -284,10 +312,10 @@ public SearchQueryBuilder join(String join, String 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.selectOnlyIdsJoinsAndFilters += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; this.joinedTables.add(join); } else if (joinedFetchedTables.contains(join) && !this.joinedTables.contains(join)) { - this.selectOnlyIds += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; + this.selectOnlyIdsJoinsAndFilters += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; } return this; } From a22c0a399083635bac879df42cedaaa649c7bfe9 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 29 Sep 2026 18:06:12 -0400 Subject: [PATCH 31/56] Removed exref ids/source, germplasm names from async check, add comment --- .../io/swagger/model/germ/GermplasmSearchRequest.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java b/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java index d1acd966..838a089c 100644 --- a/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java +++ b/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java @@ -596,6 +596,8 @@ private String toIndentedString(java.lang.Object o) { @Override @JsonIgnore + // TODO: NOTE - External Reference IDs, exref sources, germplasmNames and germplasmDbIds have been removed from the parameter count to speed up lookups and prevent async search requests. + // TODO: We would need to reconsider this if we decide to use parameter count as the way to trigger async requests when complete [BI-3021] public Integer getTotalParameterCount() { Integer count = 0; if (this.accessionNumbers != null) @@ -606,16 +608,10 @@ public Integer getTotalParameterCount() { count += this.collections.size(); if (this.commonCropNames != null) count += this.commonCropNames.size(); - if (this.externalReferenceIds != null) - count += this.externalReferenceIds.size(); - if (this.externalReferenceSources != null) - count += this.externalReferenceSources.size(); if (this.familyCodes != null) count += this.familyCodes.size(); if (this.genus != null) count += this.genus.size(); - if (this.germplasmNames != null) - count += this.germplasmNames.size(); if (this.germplasmPUIs != null) count += this.germplasmPUIs.size(); if (this.instituteCodes != null) From 1c209985139017bbc8d3b20db2fcd70ec8488838 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Wed, 30 Sep 2026 14:14:43 -0400 Subject: [PATCH 32/56] Ensure subquery uses proper alias, add more clarifying comments --- .../service/SearchQueryBuilder.java | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) 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 ad6f4a36..c40669d0 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -18,7 +18,7 @@ public class SearchQueryBuilder { private String selectClause; private String selectOnlyIds; - private String selectOnlyIdsJoinsAndFilters; + private String selectOnlyIdsSubquery; private String whereClause; private String defaultSort; private String sortClause; @@ -29,8 +29,10 @@ public class SearchQueryBuilder { public SearchQueryBuilder(Class clazz) { this.selectClause = "SELECT distinct entity FROM " + clazz.getSimpleName() + " entity "; + // This is the top level query for selectOnlyIds. It is used to construct the full id query in getIdQuery() this.selectOnlyIds = "SELECT entity.id FROM " + clazz.getSimpleName() + " entity "; - this.selectOnlyIdsJoinsAndFilters = "SELECT distinct entity.id FROM " + clazz.getSimpleName() + " entity2 "; + // This subquery will contain all the joins and filters necessary for the selectOnlyIds query and protects against duplicates using distinct + this.selectOnlyIdsSubquery = "SELECT distinct entity.id FROM " + clazz.getSimpleName() + " entity "; this.whereClause = "WHERE 1=1 "; this.defaultSort = " ORDER BY entity.id ASC "; this.sortClause = ""; @@ -57,12 +59,14 @@ public String getQuery() { * SELECT entity.id * FROM GermplasmEntity entity * WHERE entity.id IN ( - * SELECT DISTINCT entity2.id + * SELECT distinct entity2.id * FROM GermplasmEntity entity2 - * JOIN ... - * WHERE ... - * ) - * ORDER BY entity.germplasmName + * JOIN entity2.externalReferences externalReference + * WHERE 1=1 AND externalReference.externalReferenceId in :externalReferenceId + * AND externalReference.externalReferenceSource in :externalReferenceSource + * AND entity2.program.id in :program_id + * ) + * ORDER BY entity.id ASC * * By containing the filtering and the joining in a subquery with a DISTINCT, we can control potential duplicates from breaking through the cracks. * Then we can apply sorting outside of this query without a distinct to avoid postgres's requirement to include every order by column in the select clause. @@ -74,7 +78,10 @@ public String getIdQuery() { sortClause = defaultSort; } - return selectOnlyIds + " WHERE entity.id IN (" + selectOnlyIdsJoinsAndFilters + whereClause + ") " + sortClause; + // This allows us to apply all the same filters/joins built up on the original entity with the inner entity. + String fullIdsSubquery = (selectOnlyIdsSubquery + whereClause).replace("entity", "entity2"); + + return selectOnlyIds + " WHERE entity.id IN (" + fullIdsSubquery + ") " + sortClause; } public Map getParams() { @@ -312,10 +319,10 @@ public SearchQueryBuilder join(String join, String name) { if (!this.joinedTables.contains(join) && !this.joinedFetchedTables.contains(join)) { this.selectClause += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; - this.selectOnlyIdsJoinsAndFilters += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; + this.selectOnlyIdsSubquery += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; this.joinedTables.add(join); } else if (joinedFetchedTables.contains(join) && !this.joinedTables.contains(join)) { - this.selectOnlyIdsJoinsAndFilters += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; + this.selectOnlyIdsSubquery += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; } return this; } From 5b13c4eb86f39bf9e53783af83b427e8e4a742a9 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Wed, 30 Sep 2026 19:23:05 -0400 Subject: [PATCH 33/56] Support for collation on germplasm search columns --- .../model/germ/GermplasmSearchRequest.java | 8 +-- .../BrAPITestServer/model/dto/EntityType.java | 3 +- .../model/entity/germ/GermplasmEntity.java | 32 ++++++++---- .../service/SearchQueryBuilder.java | 12 +++++ .../U006_004__drop_germplasm_collation.sql | 18 +++++++ .../V006_004__collate_germplasm_columns.sql | 52 +++++++++++++++++++ 6 files changed, 109 insertions(+), 16 deletions(-) create mode 100644 src/main/resources/db/migration/U006_004__drop_germplasm_collation.sql create mode 100644 src/main/resources/db/migration/V006_004__collate_germplasm_columns.sql diff --git a/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java b/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java index 838a089c..8fbf48e2 100644 --- a/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java +++ b/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java @@ -18,14 +18,14 @@ public class GermplasmSearchRequest extends SearchRequest { private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = Map.ofEntries( // accessionNumber = GID for DeltaBreed - Map.entry("accessionNumber", new EntityColumnNameAndType("accessionNumber", EntityType.NUMBER)), + Map.entry("accessionNumber", new EntityColumnNameAndType("accessionNumber", EntityType.TEXT)), Map.entry("defaultDisplayName", new EntityColumnNameAndType("defaultDisplayName", EntityType.TEXT)), Map.entry("breedingMethod", new EntityColumnNameAndType("methodName", EntityType.TEXT)), Map.entry("seedSource", new EntityColumnNameAndType("seedSource", EntityType.TEXT)), Map.entry("pedigree",new EntityColumnNameAndType("pedigree.pedigreeString", EntityType.TEXT)), - Map.entry("femaleParentGID", new EntityColumnNameAndType("femaleParentGid", EntityType.NUMBER)), - Map.entry("maleParentGID", new EntityColumnNameAndType("maleParentGid", EntityType.NUMBER)), - Map.entry("createdDate", new EntityColumnNameAndType("createdDate", EntityType.TEXT)), + Map.entry("femaleParentGID", new EntityColumnNameAndType("femaleParentGid", EntityType.TEXT)), + Map.entry("maleParentGID", new EntityColumnNameAndType("maleParentGid", EntityType.TEXT)), + Map.entry("createdDate", new EntityColumnNameAndType("createdDate", EntityType.DATE)), Map.entry("createdBy", new EntityColumnNameAndType("createdBy", EntityType.TEXT)), Map.entry("importEntryNumber", new EntityColumnNameAndType("importEntryNumber", EntityType.NUMBER)), Map.entry("synonyms", new EntityColumnNameAndType("*synonyms.synonym", EntityType.TEXT)) 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 index 8f1156a8..10cd9c20 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityType.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityType.java @@ -5,6 +5,7 @@ public enum EntityType { UUID, BOOLEAN, // Sorting is done at the database schema/formula level for numbers. See GermplasmEntity.maleParentGid. No changes to filtering required for this datatype. - NUMBER + NUMBER, + DATE // 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/germ/GermplasmEntity.java b/src/main/java/org/brapi/test/BrAPITestServer/model/entity/germ/GermplasmEntity.java index 821c2873..59315751 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 @@ -15,6 +15,7 @@ import org.brapi.test.BrAPITestServer.model.entity.pheno.TaxonEntity; import org.hibernate.annotations.BatchSize; +import org.hibernate.annotations.Collate; import org.hibernate.annotations.Formula; import org.hibernate.annotations.Where; @@ -26,7 +27,8 @@ @Table(name = "germplasm") @Where(clause = "soft_deleted = false") public class GermplasmEntity extends BrAPIPrimaryEntity { - @Column + @Collate(value = "natural_sort") + @Column private String accessionNumber; @Column private Date acquisitionDate; @@ -45,7 +47,8 @@ public class GermplasmEntity extends BrAPIPrimaryEntity { private String countryOfOriginCode; @ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY) private CropEntity crop; - @Column + @Collate(value = "natural_sort") + @Column private String defaultDisplayName; @Column private String documentationURL; @@ -73,7 +76,8 @@ public class GermplasmEntity extends BrAPIPrimaryEntity { @ManyToMany @JoinTable private List searchResults; - @Column + @Collate(value = "natural_sort") + @Column private String seedSource; @Column private String seedSourceDescription; @@ -101,20 +105,26 @@ public class GermplasmEntity extends BrAPIPrimaryEntity { @ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY) private ProgramEntity program; - @Formula("(additional_info #>> '{createdDate}')") - private String createdDate; + @Column(name = "created_date", insertable = false, updatable = false) + private Date createdDate; - @Formula("(additional_info #>> '{createdBy,userName}')") + @Collate(value = "natural_sort") + @Column(name = "created_by", insertable = false, updatable = false) private String createdBy; - @Formula("(additional_info #>> '{breedingMethod}')") + // BI is currently not using BrAPI's breeding_method table, and is storing the data in additional_info. + // This column is being used for sorting/filtering in the meantime to get the collate/not null required for front end sorting + @Collate(value = "natural_sort") + @Column(name = "breeding_method_sort", insertable = false, updatable = false) private String methodName; - @Formula("cast(additional_info #>> '{femaleParentGid}' AS INTEGER)") - private Integer femaleParentGid; + @Collate(value = "natural_sort") + @Column(name = "female_parent_gid", insertable = false, updatable = false) + private String femaleParentGid; - @Formula("cast(additional_info #>> '{maleParentGid}' AS INTEGER)") - private Integer maleParentGid; + @Collate(value = "natural_sort") + @Column(name = "male_parent_gid", insertable = false, updatable = false) + private String maleParentGid; @Formula("cast(additional_info #>> '{importEntryNumber}' AS INTEGER)") private Integer importEntryNumber; 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 c40669d0..f4c06945 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -191,6 +191,16 @@ public SearchQueryBuilder appendLikeString(String like, String columnName) { return this; } + public SearchQueryBuilder appendLikeDate(String like, String columnName) { + String paramName = paramFilterPattern(columnName); + + if (like != null) { + this.whereClause += " AND to_char(" + entityPrefix(columnName) + ", 'YYYY-MM-DD') LIKE :" + paramName + " "; + this.params.put(paramName, "%" + like + "%"); + } + return this; + } + public > SearchQueryBuilder appendEnum(E enumVal, String columnName) { String paramName = paramFilter(columnName); if (enumVal != null) { @@ -487,6 +497,8 @@ public SearchQueryBuilder filterBy(List filterBy, Map> '{maleParentGid}', '')) STORED ; + +ALTER TABLE germplasm + ADD COLUMN + female_parent_gid text NOT NULL COLLATE natural_sort + GENERATED ALWAYS AS (COALESCE(additional_info #>> '{femaleParentGid}', '')) STORED ; + +ALTER TABLE germplasm + ADD COLUMN + breeding_method_sort text NOT NULL COLLATE natural_sort + GENERATED ALWAYS AS (COALESCE(additional_info #>> '{breedingMethod}', '')) STORED ; + +ALTER TABLE germplasm + ADD COLUMN created_date TIMESTAMP NOT NULL + GENERATED ALWAYS AS ( + make_timestamp( + substring(additional_info #>> '{createdDate}' from 7 for 4)::int, -- year + substring(additional_info #>> '{createdDate}' from 4 for 2)::int, -- month + substring(additional_info #>> '{createdDate}' from 1 for 2)::int, -- day + substring(additional_info #>> '{createdDate}' from 12 for 2)::int, -- hour + substring(additional_info #>> '{createdDate}' from 15 for 2)::int, -- minute + substring(additional_info #>> '{createdDate}' from 18 for 2)::int -- second + ) + ) STORED; + +ALTER TABLE germplasm + ADD COLUMN + created_by text NOT NULL COLLATE natural_sort + GENERATED ALWAYS AS (COALESCE(additional_info #>> '{createdBy,userName}', '')) STORED ; From 551a5b284724d2c70964abb797018600d8b3e7a6 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Thu, 1 Oct 2026 16:38:02 -0400 Subject: [PATCH 34/56] Use exact matching for NUMBER data types --- .../java/io/swagger/model/germ/GermplasmSearchRequest.java | 2 +- .../test/BrAPITestServer/service/SearchQueryBuilder.java | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java b/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java index 8fbf48e2..ed030625 100644 --- a/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java +++ b/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java @@ -18,7 +18,7 @@ public class GermplasmSearchRequest extends SearchRequest { private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = Map.ofEntries( // accessionNumber = GID for DeltaBreed - Map.entry("accessionNumber", new EntityColumnNameAndType("accessionNumber", EntityType.TEXT)), + Map.entry("accessionNumber", new EntityColumnNameAndType("accessionNumber", EntityType.NUMBER)), Map.entry("defaultDisplayName", new EntityColumnNameAndType("defaultDisplayName", EntityType.TEXT)), Map.entry("breedingMethod", new EntityColumnNameAndType("methodName", EntityType.TEXT)), Map.entry("seedSource", new EntityColumnNameAndType("seedSource", EntityType.TEXT)), 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 f4c06945..c06e67b6 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -495,8 +495,11 @@ public SearchQueryBuilder filterBy(List filterBy, Map Date: Mon, 5 Oct 2026 15:30:59 -0400 Subject: [PATCH 35/56] Implement regex expression for entity replacement for more fine grained usage --- .../BrAPITestServer/service/SearchQueryBuilder.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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 c40669d0..a7d5b3f0 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -32,7 +32,7 @@ public SearchQueryBuilder(Class clazz) { // This is the top level query for selectOnlyIds. It is used to construct the full id query in getIdQuery() this.selectOnlyIds = "SELECT entity.id FROM " + clazz.getSimpleName() + " entity "; // This subquery will contain all the joins and filters necessary for the selectOnlyIds query and protects against duplicates using distinct - this.selectOnlyIdsSubquery = "SELECT distinct entity.id FROM " + clazz.getSimpleName() + " entity "; + this.selectOnlyIdsSubquery = "SELECT distinct entity2.id FROM " + clazz.getSimpleName() + " entity2 "; this.whereClause = "WHERE 1=1 "; this.defaultSort = " ORDER BY entity.id ASC "; this.sortClause = ""; @@ -78,9 +78,16 @@ public String getIdQuery() { sortClause = defaultSort; } - // This allows us to apply all the same filters/joins built up on the original entity with the inner entity. - String fullIdsSubquery = (selectOnlyIdsSubquery + whereClause).replace("entity", "entity2"); + // Now build the subquery to apply all the same filters/joins built up on the original entity with the inner entity. + // To do this, create a regex expression which can be used to identity all instances of an "entity" without any proceeding + // words, dots, underscores, or colons, and ignore any existing instances of entity2, which exists already in selectOnlyIdsSubquery + String entityRegex = "(? Date: Mon, 5 Oct 2026 18:27:01 -0400 Subject: [PATCH 36/56] Support for nullable defaults --- .../model/entity/germ/GermplasmEntity.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 59315751..cbf15440 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 @@ -28,8 +28,8 @@ @Where(clause = "soft_deleted = false") public class GermplasmEntity extends BrAPIPrimaryEntity { @Collate(value = "natural_sort") - @Column - private String accessionNumber; + @Column(name = "accession_number", nullable = false) + private String accessionNumber = ""; @Column private Date acquisitionDate; @Column @@ -48,8 +48,8 @@ public class GermplasmEntity extends BrAPIPrimaryEntity { @ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY) private CropEntity crop; @Collate(value = "natural_sort") - @Column - private String defaultDisplayName; + @Column(name = "default_display_name", nullable = false) + private String defaultDisplayName = ""; @Column private String documentationURL; @OneToMany(mappedBy = "germplasm", cascade = CascadeType.ALL) @@ -77,8 +77,8 @@ public class GermplasmEntity extends BrAPIPrimaryEntity { @JoinTable private List searchResults; @Collate(value = "natural_sort") - @Column - private String seedSource; + @Column(name = "seed_source", nullable = false) + private String seedSource = ""; @Column private String seedSourceDescription; @Column From 076d92fa2b4ae84e5248765b63c871ca022ea4d2 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 6 Oct 2026 17:15:50 -0400 Subject: [PATCH 37/56] Make changes to migration and filtering to support default date value --- .../service/SearchQueryBuilder.java | 5 +++- .../V006_004__collate_germplasm_columns.sql | 23 +++++++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) 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 5bbd0332..855aaf4c 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -26,6 +26,7 @@ public class SearchQueryBuilder { private List joinedTables = new ArrayList<>(); private List joinedFetchedTables = new ArrayList<>(); private Class clazz; + private final String default_date = "1970-01-01"; public SearchQueryBuilder(Class clazz) { this.selectClause = "SELECT distinct entity FROM " + clazz.getSimpleName() + " entity "; @@ -202,7 +203,9 @@ public SearchQueryBuilder appendLikeDate(String like, String columnName) { String paramName = paramFilterPattern(columnName); if (like != null) { - this.whereClause += " AND to_char(" + entityPrefix(columnName) + ", 'YYYY-MM-DD') LIKE :" + paramName + " "; + this.whereClause += " AND to_char(" + entityPrefix(columnName) + ", 'YYYY-MM-DD') LIKE :" + paramName + " " + + // Ensure we do not filter against the default date, which is effectively a NULL value for DeltaBreed's system. + " AND to_char(" + entityPrefix(columnName) + ", 'YYYY-MM-DD') <> '" + default_date + "' "; this.params.put(paramName, "%" + like + "%"); } return this; diff --git a/src/main/resources/db/migration/V006_004__collate_germplasm_columns.sql b/src/main/resources/db/migration/V006_004__collate_germplasm_columns.sql index 97084b4a..a3b0acea 100644 --- a/src/main/resources/db/migration/V006_004__collate_germplasm_columns.sql +++ b/src/main/resources/db/migration/V006_004__collate_germplasm_columns.sql @@ -36,15 +36,20 @@ ALTER TABLE germplasm ALTER TABLE germplasm ADD COLUMN created_date TIMESTAMP NOT NULL GENERATED ALWAYS AS ( - make_timestamp( - substring(additional_info #>> '{createdDate}' from 7 for 4)::int, -- year - substring(additional_info #>> '{createdDate}' from 4 for 2)::int, -- month - substring(additional_info #>> '{createdDate}' from 1 for 2)::int, -- day - substring(additional_info #>> '{createdDate}' from 12 for 2)::int, -- hour - substring(additional_info #>> '{createdDate}' from 15 for 2)::int, -- minute - substring(additional_info #>> '{createdDate}' from 18 for 2)::int -- second - ) - ) STORED; + CASE + WHEN additional_info #>> '{createdDate}' IS NULL + THEN TIMESTAMP '1970-01-01 00:00:00' + ELSE + make_timestamp( + substring(additional_info #>> '{createdDate}' from 7 for 4)::int, -- year + substring(additional_info #>> '{createdDate}' from 4 for 2)::int, -- month + substring(additional_info #>> '{createdDate}' from 1 for 2)::int, -- day + substring(additional_info #>> '{createdDate}' from 12 for 2)::int, -- hour + substring(additional_info #>> '{createdDate}' from 15 for 2)::int, -- minute + substring(additional_info #>> '{createdDate}' from 18 for 2)::int -- second + ) + END + ) STORED; ALTER TABLE germplasm ADD COLUMN From bcf87be2d4e8768c4ec03fc03030413aa12d9d17 Mon Sep 17 00:00:00 2001 From: humsika Date: Mon, 12 Oct 2026 16:02:28 -0500 Subject: [PATCH 38/56] BI-2964: Updated the logic of how the pages are calculate to avoid getting inaccurate data in the response. --- .../primaryEntities/BrAPIRepositoryImpl.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java index 583a28cf..364cfd8d 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java @@ -240,10 +240,12 @@ private Long getTotalCount(SearchQueryBuilder searchQuery) { return 0L; } - private void validatePageNumber(Pageable pageReq, Long totalCount) throws InvalidPagingException { - int reqPageNum = pageReq.getPageNumber(); - if (reqPageNum != 0 && reqPageNum > Math.floor(((double) totalCount) / pageReq.getPageSize())) { - // Condition indicates a request was sent where the page requested exceeds the total number of pages. + static void validatePageNumber(Pageable pageReq, Long totalCount) throws InvalidPagingException { + int requestedPage = pageReq.getPageNumber(); + long totalPages = (long) Math.ceil(totalCount / (double) pageReq.getPageSize()); + + // Condition indicates a request was sent where the page requested exceeds the total number of pages. + if (requestedPage > 0 && requestedPage >= totalPages) { // Instruct requester to send the correct request. throw new InvalidPagingException(); } From 091f833e37bf9778fed873d1b6a8599351de0b04 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 12 Oct 2026 19:56:03 -0400 Subject: [PATCH 39/56] Lay groundwork for generic ListItem implementation --- .../java/io/swagger/api/core/ListsApi.java | 18 ++- .../core/GermplasmListSearchRequest.java | 35 +++++ .../java/io/swagger/model/core/ListItem.java | 100 ++++++++++++++ .../model/core/ListItemSearchRequest.java | 21 +++ .../model/core/ListItemWithFullItem.java | 82 ++++++++++++ .../model/core/ListItemsListResponse.java | 122 ++++++++++++++++++ .../core/ListItemsListResponseResult.java | 87 +++++++++++++ .../model/germ/GermplasmSearchRequest.java | 9 +- .../controller/core/ListsApiController.java | 21 ++- .../GermplasmListSearchRequestHandler.java | 36 ++++++ .../ListItemSearchRequestHandler.java | 20 +++ .../service/core/ListItemService.java | 59 +++++++++ .../service/germ/GermplasmService.java | 7 + 13 files changed, 607 insertions(+), 10 deletions(-) create mode 100644 src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java create mode 100644 src/main/java/io/swagger/model/core/ListItem.java create mode 100644 src/main/java/io/swagger/model/core/ListItemSearchRequest.java create mode 100644 src/main/java/io/swagger/model/core/ListItemWithFullItem.java create mode 100644 src/main/java/io/swagger/model/core/ListItemsListResponse.java create mode 100644 src/main/java/io/swagger/model/core/ListItemsListResponseResult.java create mode 100644 src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java create mode 100644 src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java create mode 100644 src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java diff --git a/src/main/java/io/swagger/api/core/ListsApi.java b/src/main/java/io/swagger/api/core/ListsApi.java index b4c95e98..71acc643 100644 --- a/src/main/java/io/swagger/api/core/ListsApi.java +++ b/src/main/java/io/swagger/api/core/ListsApi.java @@ -7,11 +7,7 @@ import io.swagger.model.BrAPIResponse; import io.swagger.model.Model202AcceptedSearchResponse; -import io.swagger.model.core.ListNewRequest; -import io.swagger.model.core.ListResponse; -import io.swagger.model.core.ListSearchRequest; -import io.swagger.model.core.ListsListResponse; -import io.swagger.model.core.ListsSingleResponse; +import io.swagger.model.core.*; import io.swagger.annotations.*; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; @@ -166,4 +162,16 @@ ResponseEntity searchListsSearchResultsDbIdGet( @ApiParam(value = "pageSize") @Valid @RequestParam(value = "pageSize", required = false) Integer pageSize, @ApiParam(value = "HTTP HEADER - Token used for Authorization Bearer {token_string} ") @RequestHeader(value = "Authorization", required = false) String authorization) throws BrAPIServerException; + @ApiOperation(value = "Submit a search request for List Items", nickname = "searchListByItemRequest", notes = "Advanced searching for the list item resource. See Search Services for additional implementation details.", response = ListItemsListResponse.class, authorizations = { + @Authorization(value = "AuthorizationToken") }, tags = { "Lists", }) + @ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = ListItemsListResponse.class), + @ApiResponse(code = 202, message = "Accepted", response = Model202AcceptedSearchResponse.class), + @ApiResponse(code = 400, message = "Bad Request", response = String.class), + @ApiResponse(code = 401, message = "Unauthorized", response = String.class), + @ApiResponse(code = 403, message = "Forbidden", response = String.class) }) + @RequestMapping(value = "/search/lists/{listDbId}/items", produces = { "application/json" }, consumes = { + "application/json" }, method = RequestMethod.POST) + ResponseEntity searchListByItemRequest(@ApiParam(value = "") @Valid @RequestBody ListItemSearchRequest body, + @ApiParam(value = "Permanent unique identifier of list to perform search on", required = true) @PathVariable("listDbId") String listDbId, + @ApiParam(value = "HTTP HEADER - Token used for Authorization Bearer {token_string} ") @RequestHeader(value = "Authorization", required = false) String authorization) throws BrAPIServerException; } diff --git a/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java b/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java new file mode 100644 index 00000000..2d50ac7c --- /dev/null +++ b/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java @@ -0,0 +1,35 @@ +package io.swagger.model.core; + +import io.swagger.model.germ.GermplasmSearchRequest; +import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; +import org.brapi.test.BrAPITestServer.model.dto.EntityType; + +import java.util.Collections; +import java.util.Map; + +public class GermplasmListSearchRequest extends GermplasmSearchRequest { + + // The only real difference in this request is that there's an extra sort/filter field that GermplasmSearchRequest doesn't have. + // It is specific to lists. + private static final Map + LIST_ALLOWED_FIELDS; + + static { + Map fields = + copyAllowedSortAndFilterFields(); + + fields.put( + "listItemPosition", + new EntityColumnNameAndType("listItem.position", EntityType.NUMBER) + ); + + LIST_ALLOWED_FIELDS = Collections.unmodifiableMap(fields); + } + + @Override + public Map getEntityColAndTypeBySubmittedNameMap() { + return LIST_ALLOWED_FIELDS; + } + + +} diff --git a/src/main/java/io/swagger/model/core/ListItem.java b/src/main/java/io/swagger/model/core/ListItem.java new file mode 100644 index 00000000..0627eb7b --- /dev/null +++ b/src/main/java/io/swagger/model/core/ListItem.java @@ -0,0 +1,100 @@ +package io.swagger.model.core; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import org.springframework.validation.annotation.Validated; + +/** + * ListItem + */ +@Validated +@javax.annotation.processing.Generated(value = "io.swagger.codegen.v3.generators.java.SpringCodegen", date = "2020-03-20T16:31:52.030Z[GMT]") +public class ListItem { + @JsonProperty("listItemDbId") + private String listItemDbId = null; + + @JsonProperty("position") + private Integer position = null; + + // This represents the actual value stored in the list_item table + @JsonProperty("item") + private String item = null; + + public ListItem listDbId(String listDbId) { + this.listItemDbId = listDbId; + return this; + } + + /** + * The unique identifier for a List + * @return listDbId + **/ + @ApiModelProperty(example = "6f621cfa", required = true, value = "The unique identifier for a List") + + + public String getListItemDbId() { + return listItemDbId; + } + + public void setListItemDbId(String listItemDbId) { + this.listItemDbId = listItemDbId; + } + + public Integer getPosition() { + return position; + } + + public void setPosition(Integer position) { + this.position = position; + } + + public String getItem() { + return item; + } + + public void setItem(String item) { + this.item = item; + } + + @Override + public boolean equals(java.lang.Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ListItem ListItem = (ListItem) o; + return Objects.equals(this.listItemDbId, ListItem.listItemDbId) && + super.equals(o); + } + + @Override + public int hashCode() { + return Objects.hash(listItemDbId, super.hashCode()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ListItem {\n"); + sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + sb.append(" listDbId: ").append(toIndentedString(listItemDbId)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + + diff --git a/src/main/java/io/swagger/model/core/ListItemSearchRequest.java b/src/main/java/io/swagger/model/core/ListItemSearchRequest.java new file mode 100644 index 00000000..dc9e4949 --- /dev/null +++ b/src/main/java/io/swagger/model/core/ListItemSearchRequest.java @@ -0,0 +1,21 @@ +package io.swagger.model.core; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.model.SearchRequest; + +public class ListItemSearchRequest { + + @JsonProperty("searchRequest") + private JsonNode searchRequest; + + // List-item-specific filters and sorting can go here. + + public JsonNode getSearchRequest() { + return searchRequest; + } + + public void setSearchRequest(JsonNode searchRequest) { + this.searchRequest = searchRequest; + } +} diff --git a/src/main/java/io/swagger/model/core/ListItemWithFullItem.java b/src/main/java/io/swagger/model/core/ListItemWithFullItem.java new file mode 100644 index 00000000..bacfb5bf --- /dev/null +++ b/src/main/java/io/swagger/model/core/ListItemWithFullItem.java @@ -0,0 +1,82 @@ +package io.swagger.model.core; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import org.springframework.validation.annotation.Validated; + +/** + * ListItemWithFullItem + */ +@Validated +@javax.annotation.processing.Generated(value = "io.swagger.codegen.v3.generators.java.SpringCodegen", date = "2020-03-20T16:31:52.030Z[GMT]") +public class ListItemWithFullItem { + @JsonProperty("listItem") + private ListItem listItem = null; + + @JsonProperty("item") + private T item = null; + + /** + * The unique identifier for a List + * @return listItem + **/ + @ApiModelProperty(example = "6f621cfa", required = true, value = "The unique identifier for a List") + + + public ListItem getlistItem() { + return listItem; + } + + public void setlistItem(ListItem listItem) { + this.listItem = listItem; + } + + public T getItem() { + return item; + } + + public void setItem(T item) { + this.item = item; + } + + @Override + public boolean equals(java.lang.Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ListItemWithFullItem listItemWithFullItem = (ListItemWithFullItem) o; + return Objects.equals(this.listItem, listItemWithFullItem.listItem) && + super.equals(o); + } + + @Override + public int hashCode() { + return Objects.hash(listItem, super.hashCode()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ListItemWithFullItem {\n"); + sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + sb.append(" listItem: ").append(toIndentedString(listItem)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/src/main/java/io/swagger/model/core/ListItemsListResponse.java b/src/main/java/io/swagger/model/core/ListItemsListResponse.java new file mode 100644 index 00000000..e36f43e7 --- /dev/null +++ b/src/main/java/io/swagger/model/core/ListItemsListResponse.java @@ -0,0 +1,122 @@ +package io.swagger.model.core; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import io.swagger.model.BrAPIResponse; +import io.swagger.model.Context; +import io.swagger.model.Metadata; + +import org.springframework.validation.annotation.Validated; +import jakarta.validation.Valid; + +/** + * ListItemsListResponse + */ +@Validated +@javax.annotation.processing.Generated(value = "io.swagger.codegen.v3.generators.java.SpringCodegen", date = "2020-03-20T16:31:52.030Z[GMT]") +public class ListItemsListResponse implements BrAPIResponse { + @JsonProperty("@context") + private Context _atContext = null; + + @JsonProperty("metadata") + private Metadata metadata = null; + + @JsonProperty("result") + private ListItemsListResponseResult result = null; + + public ListItemsListResponse() { + this._atContext = new Context(); + this._atContext.add("context"); + } + + + public void set_atContext(Context _atContext) { + this._atContext = _atContext; + } + public ListItemsListResponse metadata(Metadata metadata) { + this.metadata = metadata; + return this; + } + + /** + * Get metadata + * + * @return metadata + **/ + @ApiModelProperty(required = true, value = "") + + + @Valid + public Metadata getMetadata() { + return metadata; + } + + public void setMetadata(Metadata metadata) { + this.metadata = metadata; + } + + public ListItemsListResponse result(ListItemsListResponseResult result) { + this.result = result; + return this; + } + + /** + * Get result + * + * @return result + **/ + @ApiModelProperty(required = true, value = "") + + + @Valid + public ListItemsListResponseResult getResult() { + return result; + } + + public void setResult(ListItemsListResponseResult result) { + this.result = result; + } + + @Override + public boolean equals(java.lang.Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ListItemsListResponse listItemsListResponse = (ListItemsListResponse) o; + return Objects.equals(this._atContext, listItemsListResponse._atContext) + && Objects.equals(this.metadata, listItemsListResponse.metadata) + && Objects.equals(this.result, listItemsListResponse.result); + } + + @Override + public int hashCode() { + return Objects.hash(_atContext, metadata, result); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ListItemsListResponse {\n"); + + sb.append(" _atContext: ").append(toIndentedString(_atContext)).append("\n"); + sb.append(" metadata: ").append(toIndentedString(metadata)).append("\n"); + sb.append(" result: ").append(toIndentedString(result)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/src/main/java/io/swagger/model/core/ListItemsListResponseResult.java b/src/main/java/io/swagger/model/core/ListItemsListResponseResult.java new file mode 100644 index 00000000..6a5f6561 --- /dev/null +++ b/src/main/java/io/swagger/model/core/ListItemsListResponseResult.java @@ -0,0 +1,87 @@ +package io.swagger.model.core; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import io.swagger.model.BrAPIResponseResult; + +import java.util.ArrayList; +import java.util.List; +import org.springframework.validation.annotation.Validated; +import jakarta.validation.Valid; + +/** + * ListItemsListResponseResult + */ +@Validated +@javax.annotation.processing.Generated(value = "io.swagger.codegen.v3.generators.java.SpringCodegen", date = "2020-03-20T16:31:52.030Z[GMT]") +public class ListItemsListResponseResult implements BrAPIResponseResult> { + @JsonProperty("data") + @Valid + private List> data = new ArrayList>(); + + public ListItemsListResponseResult data(List> data) { + this.data = data; + return this; + } + + public ListItemsListResponseResult addDataItem(ListItemWithFullItem dataItem) { + this.data.add(dataItem); + return this; + } + + /** + * Get data + * @return data + **/ + @ApiModelProperty(required = true, value = "") + + @Valid + public List> getData() { + return data; + } + + public void setData(List> data) { + this.data = data; + } + + + @Override + public boolean equals(java.lang.Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ListItemsListResponseResult listItemsListResponseResult = (ListItemsListResponseResult) o; + return Objects.equals(this.data, listItemsListResponseResult.data); + } + + @Override + public int hashCode() { + return Objects.hash(data); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ListItemsListResponseResult {\n"); + + sb.append(" data: ").append(toIndentedString(data)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java b/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java index ed030625..029ab807 100644 --- a/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java +++ b/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java @@ -6,10 +6,7 @@ import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; import org.brapi.test.BrAPITestServer.model.dto.EntityType; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; public class GermplasmSearchRequest extends SearchRequest { @@ -31,6 +28,10 @@ public class GermplasmSearchRequest extends SearchRequest { Map.entry("synonyms", new EntityColumnNameAndType("*synonyms.synonym", EntityType.TEXT)) ); + protected static Map copyAllowedSortAndFilterFields() { + return new HashMap<>(ALLOWED_SORT_AND_FILTER_FIELDS); + } + @JsonProperty("accessionNumbers") private List accessionNumbers = null; diff --git a/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java b/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java index ce653cdf..92af758a 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java @@ -12,6 +12,7 @@ import org.brapi.test.BrAPITestServer.model.entity.SearchRequestEntity; import org.brapi.test.BrAPITestServer.model.entity.SearchRequestEntity.SearchRequestTypes; import org.brapi.test.BrAPITestServer.service.SearchService; +import org.brapi.test.BrAPITestServer.service.core.ListItemService; import org.brapi.test.BrAPITestServer.service.core.ListService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,12 +40,14 @@ public class ListsApiController extends BrAPIController implements ListsApi { private final HttpServletRequest request; private final ListService listService; private final SearchService searchService; + private final ListItemService listItemService; @Autowired - public ListsApiController(ListService listService, SearchService searchService, HttpServletRequest request) { + public ListsApiController(ListService listService, SearchService searchService, HttpServletRequest request, ListItemService listItemService) { this.listService = listService; this.searchService = searchService; this.request = request; + this.listItemService = listItemService; } @CrossOrigin @@ -205,4 +208,20 @@ public ResponseEntity searchListsSearchResultsDbIdGet( } } + @CrossOrigin + @Override + public ResponseEntity searchListByItemRequest( + @Valid @RequestBody ListItemSearchRequest body, + @PathVariable("listDbId") String listDbId, + @RequestHeader(value = "Authorization", required = false) String authorization) throws BrAPIServerException { + + log.debug("Request: " + request.getRequestURI()); + validateSecurityContext(request, "ROLE_ANONYMOUS", "ROLE_USER"); + validateAcceptHeader(request); + Metadata metadata = generateEmptyMetadata(); + + List> data = listItemService.search(listDbId, body); + return responseOK(new ListItemsListResponse(), new ListItemsListResponseResult(), data, metadata); + } + } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java b/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java new file mode 100644 index 00000000..9b7ffe94 --- /dev/null +++ b/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java @@ -0,0 +1,36 @@ +package org.brapi.test.BrAPITestServer.handlers; + +import io.swagger.model.core.GermplasmListSearchRequest; +import io.swagger.model.core.ListDetails; +import io.swagger.model.core.ListItemWithFullItem; +import io.swagger.model.core.ListTypes; +import org.brapi.test.BrAPITestServer.service.germ.GermplasmService; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class GermplasmListSearchRequestHandler implements ListItemSearchRequestHandler { + private final GermplasmService germplasmService; + + public GermplasmListSearchRequestHandler(GermplasmService germplasmService) { + this.germplasmService = germplasmService; + } + + @Override + public ListTypes getListType() { + return ListTypes.GERMPLASM; + } + + @Override + public Class getSearchRequestClass() { + return GermplasmListSearchRequest.class; + } + + @Override + public List> search( + ListDetails list, + GermplasmListSearchRequest searchRequest) { + return germplasmService.searchGermplasmByList(list, searchRequest); + } +} diff --git a/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java b/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java new file mode 100644 index 00000000..ef0fe896 --- /dev/null +++ b/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java @@ -0,0 +1,20 @@ +package org.brapi.test.BrAPITestServer.handlers; + +import io.swagger.model.core.ListDetails; +import io.swagger.model.core.ListItemWithFullItem; +import io.swagger.model.core.ListItemsListResponseResult; +import io.swagger.model.core.ListTypes; +import org.brapi.test.BrAPITestServer.model.entity.core.ListEntity; + +import java.util.List; + +public interface ListItemSearchRequestHandler { + ListTypes getListType(); + + Class getSearchRequestClass(); + + List> search( + ListDetails list, + T searchRequest + ); +} diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java new file mode 100644 index 00000000..d1f17c3c --- /dev/null +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java @@ -0,0 +1,59 @@ +package org.brapi.test.BrAPITestServer.service.core; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.swagger.model.core.*; +import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; +import org.brapi.test.BrAPITestServer.handlers.ListItemSearchRequestHandler; +import org.brapi.test.BrAPITestServer.model.entity.core.ListEntity; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +public class ListItemService { + private final ListService listService; + private final Map> handlers; + private final ObjectMapper objectMapper; + + public ListItemService(ListService listService, + List> handlerList, + ObjectMapper objectMapper) { + this.listService = listService; + this.objectMapper = objectMapper; + this.handlers = handlerList.stream() + .collect(Collectors.toMap( + ListItemSearchRequestHandler::getListType, + Function.identity() + )); + } + + public List> search(String listDbId, + ListItemSearchRequest searchRequest) throws BrAPIServerException { + ListDetails list = listService.getList(listDbId); + + ListItemSearchRequestHandler handler = handlers.get(list.getListType()); + + if (handler == null) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "Unsupported list type: " + list.getListType()); + } + + return executeHandler(handler, list, searchRequest); + } + + private List> executeHandler( + ListItemSearchRequestHandler handler, + ListDetails list, + ListItemSearchRequest request) { + + T typedSearchRequest = objectMapper.convertValue( + request.getSearchRequest(), + handler.getSearchRequestClass() + ); + + return handler.search(list, typedSearchRequest); + } +} 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 36485c6d..c5ac8e80 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,9 @@ import java.util.*; import java.util.stream.Collectors; +import io.swagger.model.core.GermplasmListSearchRequest; +import io.swagger.model.core.ListDetails; +import io.swagger.model.core.ListItemWithFullItem; import io.swagger.model.core.ProgramSearchRequest; import io.swagger.model.germ.*; import jakarta.validation.Valid; @@ -455,6 +458,10 @@ private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder> searchGermplasmByList(ListDetails list, GermplasmListSearchRequest request) { + return new ArrayList<>(); + } + public Germplasm getGermplasm(String germplasmDbId) throws BrAPIServerException { return convertFromEntity(getGermplasmEntity(germplasmDbId, HttpStatus.NOT_FOUND)); } From 5e18e83c45c8f36378ff9a7efafd15552a2c88d9 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 13 Oct 2026 13:28:47 -0400 Subject: [PATCH 40/56] Fix some bugs, add working example query --- .../java/io/swagger/api/core/ListsApi.java | 3 +- .../model/core/ListItemSearchRequest.java | 1 - .../controller/core/BrAPIController.java | 20 +++++++++ .../controller/core/ListsApiController.java | 7 ++-- .../GermplasmListSearchRequestHandler.java | 42 ++++++++++++++++--- .../ListItemSearchRequestHandler.java | 9 ++-- .../primaryEntities/BrAPIRepository.java | 4 +- .../primaryEntities/BrAPIRepositoryImpl.java | 13 ++++++ .../service/SearchQueryBuilder.java | 26 ++++++++++++ .../service/core/ListItemService.java | 38 +++++++++++------ .../service/germ/GermplasmService.java | 9 +++- 11 files changed, 143 insertions(+), 29 deletions(-) diff --git a/src/main/java/io/swagger/api/core/ListsApi.java b/src/main/java/io/swagger/api/core/ListsApi.java index 71acc643..8d346d29 100644 --- a/src/main/java/io/swagger/api/core/ListsApi.java +++ b/src/main/java/io/swagger/api/core/ListsApi.java @@ -5,6 +5,7 @@ */ package io.swagger.api.core; +import com.fasterxml.jackson.databind.JsonNode; import io.swagger.model.BrAPIResponse; import io.swagger.model.Model202AcceptedSearchResponse; import io.swagger.model.core.*; @@ -171,7 +172,7 @@ ResponseEntity searchListsSearchResultsDbIdGet( @ApiResponse(code = 403, message = "Forbidden", response = String.class) }) @RequestMapping(value = "/search/lists/{listDbId}/items", produces = { "application/json" }, consumes = { "application/json" }, method = RequestMethod.POST) - ResponseEntity searchListByItemRequest(@ApiParam(value = "") @Valid @RequestBody ListItemSearchRequest body, + ResponseEntity searchListByItemRequest(@ApiParam(value = "") @Valid @RequestBody JsonNode body, @ApiParam(value = "Permanent unique identifier of list to perform search on", required = true) @PathVariable("listDbId") String listDbId, @ApiParam(value = "HTTP HEADER - Token used for Authorization Bearer {token_string} ") @RequestHeader(value = "Authorization", required = false) String authorization) throws BrAPIServerException; } diff --git a/src/main/java/io/swagger/model/core/ListItemSearchRequest.java b/src/main/java/io/swagger/model/core/ListItemSearchRequest.java index dc9e4949..c5fe9b33 100644 --- a/src/main/java/io/swagger/model/core/ListItemSearchRequest.java +++ b/src/main/java/io/swagger/model/core/ListItemSearchRequest.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; -import io.swagger.model.SearchRequest; public class ListItemSearchRequest { diff --git a/src/main/java/org/brapi/test/BrAPITestServer/controller/core/BrAPIController.java b/src/main/java/org/brapi/test/BrAPITestServer/controller/core/BrAPIController.java index 05cd18bf..42099bcd 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/controller/core/BrAPIController.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/controller/core/BrAPIController.java @@ -5,6 +5,7 @@ import java.util.Set; import java.util.stream.Collectors; +import com.fasterxml.jackson.databind.JsonNode; import jakarta.servlet.http.HttpServletRequest; import io.swagger.model.core.BatchDeletesListResponse; @@ -49,6 +50,25 @@ protected Metadata generateMetaDataTemplate(String pageToken, Integer pageSize) return metaData; } + protected Metadata generateMetaDataTemplate(JsonNode node) throws BrAPIServerException { + + Integer page = null; + Integer pageSize = null; + + JsonNode pageAsNode = node.get("page"); + JsonNode pageSizeAsNode = node.get("pageSize"); + + if (pageAsNode != null && pageAsNode.canConvertToInt()) { + page = pageAsNode.asInt(); + } + + if (pageSizeAsNode != null && pageSizeAsNode.canConvertToInt()) { + pageSize = pageSizeAsNode.asInt(); + } + + return generateMetaDataTemplate(page, pageSize); + } + protected Metadata generateMetaDataTemplate(Integer page, Integer pageSize) throws BrAPIServerException { PagingUtility.validatePaging(page, pageSize); diff --git a/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java b/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java index 92af758a..4c8fd78d 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java @@ -1,5 +1,6 @@ package org.brapi.test.BrAPITestServer.controller.core; +import com.fasterxml.jackson.databind.JsonNode; import io.swagger.model.BrAPIResponse; import io.swagger.model.Metadata; import io.swagger.model.core.*; @@ -211,16 +212,16 @@ public ResponseEntity searchListsSearchResultsDbIdGet( @CrossOrigin @Override public ResponseEntity searchListByItemRequest( - @Valid @RequestBody ListItemSearchRequest body, + @Valid @RequestBody JsonNode body, @PathVariable("listDbId") String listDbId, @RequestHeader(value = "Authorization", required = false) String authorization) throws BrAPIServerException { log.debug("Request: " + request.getRequestURI()); validateSecurityContext(request, "ROLE_ANONYMOUS", "ROLE_USER"); validateAcceptHeader(request); - Metadata metadata = generateEmptyMetadata(); + Metadata metadata = generateMetaDataTemplate(body); - List> data = listItemService.search(listDbId, body); + List> data = listItemService.search(listDbId, body, metadata); return responseOK(new ListItemsListResponse(), new ListItemsListResponseResult(), data, metadata); } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java b/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java index 9b7ffe94..957d83e0 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java @@ -1,12 +1,16 @@ package org.brapi.test.BrAPITestServer.handlers; -import io.swagger.model.core.GermplasmListSearchRequest; -import io.swagger.model.core.ListDetails; -import io.swagger.model.core.ListItemWithFullItem; -import io.swagger.model.core.ListTypes; +import io.swagger.model.Metadata; +import io.swagger.model.core.*; +import io.swagger.model.germ.Germplasm; +import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; +import org.brapi.test.BrAPITestServer.model.entity.core.ListItemEntity; +import org.brapi.test.BrAPITestServer.model.entity.germ.GermplasmEntity; +import org.brapi.test.BrAPITestServer.service.PagingUtility; import org.brapi.test.BrAPITestServer.service.germ.GermplasmService; import org.springframework.stereotype.Component; +import java.util.ArrayList; import java.util.List; @Component @@ -30,7 +34,33 @@ public Class getSearchRequestClass() { @Override public List> search( ListDetails list, - GermplasmListSearchRequest searchRequest) { - return germplasmService.searchGermplasmByList(list, searchRequest); + GermplasmListSearchRequest searchRequest, + Metadata metadata) throws BrAPIServerException { + + List results = germplasmService.getGermplasmInList(); + + List> listItems = new ArrayList<>(); + + for (Object[] result : results) { + ListItemWithFullItem item = new ListItemWithFullItem<>(); + + ListItemEntity listItemEntity = (ListItemEntity) result[0]; + GermplasmEntity germplasmEntity = (GermplasmEntity) result[1]; + + ListItem listItem = new ListItem(); + listItem.setListItemDbId(listItemEntity.getId().toString()); + listItem.setItem(listItemEntity.getItem()); + listItem.setPosition(listItemEntity.getPosition()); + + item.setlistItem(listItem); + + Germplasm germplasm = germplasmService.convertFromEntity(germplasmEntity); + + item.setItem(germplasm); + + listItems.add(item); + } + + return listItems; } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java b/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java index ef0fe896..12e988ed 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java @@ -1,10 +1,10 @@ package org.brapi.test.BrAPITestServer.handlers; +import io.swagger.model.Metadata; import io.swagger.model.core.ListDetails; import io.swagger.model.core.ListItemWithFullItem; -import io.swagger.model.core.ListItemsListResponseResult; import io.swagger.model.core.ListTypes; -import org.brapi.test.BrAPITestServer.model.entity.core.ListEntity; +import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; import java.util.List; @@ -15,6 +15,7 @@ public interface ListItemSearchRequestHandler { List> search( ListDetails list, - T searchRequest - ); + T searchRequest, + Metadata metadata + ) throws BrAPIServerException; } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepository.java b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepository.java index dd76d8f4..732924f5 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepository.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepository.java @@ -27,10 +27,12 @@ public interface BrAPIRepository S save(S entity); public List saveAll(Iterable entities); - + public void refresh(S entity); public void fetchXrefs(List page, Page pagedEntities, Class searchClass) throws InvalidPagingException; List findByIdIn(List ids); + + List testMappedEntityHql(SearchQueryBuilder searchQuery); } \ No newline at end of file diff --git a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java index 583a28cf..30a4fdfb 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java @@ -248,4 +248,17 @@ private void validatePageNumber(Pageable pageReq, Long totalCount) throws Invali throw new InvalidPagingException(); } } + + public List testMappedEntityHql(SearchQueryBuilder searchQuery) { + searchQuery.findGermplasmListItems(); + TypedQuery query = entityManager.createQuery(searchQuery.testHql, Object[].class); + + for (Entry entry : searchQuery.getParams().entrySet()) { + query.setParameter(entry.getKey(), entry.getValue()); + } + + query.setFirstResult((int) 0); + query.setMaxResults(10); + return query.getResultList(); + } } 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 855aaf4c..2d34a853 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -8,6 +8,7 @@ import io.swagger.model.FilterBy; import io.swagger.model.GeoJSONSearchArea; +import io.swagger.model.core.ListTypes; import io.swagger.model.sort.SortBy; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; @@ -27,6 +28,7 @@ public class SearchQueryBuilder { private List joinedFetchedTables = new ArrayList<>(); private Class clazz; private final String default_date = "1970-01-01"; + public String testHql; public SearchQueryBuilder(Class clazz) { this.selectClause = "SELECT distinct entity FROM " + clazz.getSimpleName() + " entity "; @@ -528,4 +530,28 @@ private void joinCollectionColumn(String submittedSortFilterColumnName) { this.join(joinTableName, joinTableName); } + + public SearchQueryBuilder findGermplasmListItems() { + + testHql = """ + SELECT listItem, germplasm + FROM ListItemEntity listItem + JOIN listItem.list list + JOIN GermplasmEntity germplasm + ON germplasm.germplasmName = listItem.item + JOIN germplasm.pedigree pedigree + WHERE list.id = :listDbId + AND list.listType = :listType + AND germplasm.defaultDisplayName LIKE :defaultDisplayName + AND to_char(germplasm.createdDate, 'YYYY-MM-DD') LIKE :createdDate + ORDER BY germplasm.accessionNumber ASC, listItem.id ASC + """; + + params.put("listDbId", UUID.fromString("d200d581-1136-41e0-820d-806bf9cb63a8")); + params.put("listType", ListTypes.GERMPLASM); + params.put("defaultDisplayName", "%" + "86-MO-1-1.1" + "%"); + params.put("createdDate", "%20%"); + + return this; + } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java index d1f17c3c..bddd7a40 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java @@ -1,11 +1,13 @@ package org.brapi.test.BrAPITestServer.service.core; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import io.swagger.model.Metadata; import io.swagger.model.core.*; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; import org.brapi.test.BrAPITestServer.handlers.ListItemSearchRequestHandler; -import org.brapi.test.BrAPITestServer.model.entity.core.ListEntity; import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.stereotype.Service; import java.util.List; @@ -32,7 +34,8 @@ public ListItemService(ListService listService, } public List> search(String listDbId, - ListItemSearchRequest searchRequest) throws BrAPIServerException { + JsonNode searchRequest, + Metadata metadata) throws BrAPIServerException { ListDetails list = listService.getList(listDbId); ListItemSearchRequestHandler handler = handlers.get(list.getListType()); @@ -41,19 +44,30 @@ public List> search(String listDbId, throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "Unsupported list type: " + list.getListType()); } - return executeHandler(handler, list, searchRequest); + return executeHandler(handler, list, searchRequest, metadata); } - private List> executeHandler( - ListItemSearchRequestHandler handler, - ListDetails list, - ListItemSearchRequest request) { + private List> executeHandler(ListItemSearchRequestHandler handler, + ListDetails list, + JsonNode request, + Metadata metadata) throws BrAPIServerException { - T typedSearchRequest = objectMapper.convertValue( - request.getSearchRequest(), - handler.getSearchRequestClass() - ); + T typedSearchRequest; - return handler.search(list, typedSearchRequest); + try { + typedSearchRequest = objectMapper.convertValue( + request, + handler.getSearchRequestClass() + ); + } catch (IllegalArgumentException e) { + // This allows us to emulate the 400 request failures that normally propagate to the BrapiExceptionHandler.handleHttpMessageNotReadable() + // This is needed because of the generic nature of the EntityListSearchRequest objects, which come in as untyped JsonNode objects. + // To support EntityListSearchRequest objects of any type, the jackson serialization has to be done after where it normally occurs at the filter level. + // Throwing this type of exception obfuscates this fact, and if there are serialization/parsing issues we should see the same error messages we would normally see + // if done at the filter level. + throw new HttpMessageNotReadableException("Could not convert request object to JSON object.", e); + } + + return handler.search(list, typedSearchRequest, metadata); } } 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 c5ac8e80..33510a5d 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 @@ -527,7 +527,7 @@ public List saveGermplasm(@Valid List body) thro .collect(Collectors.toList()); } - private Germplasm convertFromEntity(GermplasmEntity entity) { + public Germplasm convertFromEntity(GermplasmEntity entity) { Germplasm germ = new Germplasm(); UpdateUtility.convertFromEntity(entity, germ); @@ -1121,4 +1121,11 @@ private List getRedundantInstitutes( return dupInstitutes; } + public List getGermplasmInList() { + + SearchQueryBuilder searchQuery = new SearchQueryBuilder<>( + GermplasmEntity.class); + return germplasmRepository.testMappedEntityHql(searchQuery); + } + } From 9fd14f8e280a8b67a2f1799fcce99ab9443dda80 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 13 Oct 2026 18:12:09 -0400 Subject: [PATCH 41/56] Clean out sample code, bolster SearchQueryBuilder attributes --- .../GermplasmListSearchRequestHandler.java | 27 +------- .../primaryEntities/BrAPIRepository.java | 3 +- .../primaryEntities/BrAPIRepositoryImpl.java | 48 ++++++++++---- .../service/SearchQueryBuilder.java | 62 +++++++------------ .../service/germ/GermplasmService.java | 29 ++++++--- 5 files changed, 79 insertions(+), 90 deletions(-) diff --git a/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java b/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java index 957d83e0..718401bb 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java @@ -6,7 +6,6 @@ import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; import org.brapi.test.BrAPITestServer.model.entity.core.ListItemEntity; import org.brapi.test.BrAPITestServer.model.entity.germ.GermplasmEntity; -import org.brapi.test.BrAPITestServer.service.PagingUtility; import org.brapi.test.BrAPITestServer.service.germ.GermplasmService; import org.springframework.stereotype.Component; @@ -37,30 +36,6 @@ public List> search( GermplasmListSearchRequest searchRequest, Metadata metadata) throws BrAPIServerException { - List results = germplasmService.getGermplasmInList(); - - List> listItems = new ArrayList<>(); - - for (Object[] result : results) { - ListItemWithFullItem item = new ListItemWithFullItem<>(); - - ListItemEntity listItemEntity = (ListItemEntity) result[0]; - GermplasmEntity germplasmEntity = (GermplasmEntity) result[1]; - - ListItem listItem = new ListItem(); - listItem.setListItemDbId(listItemEntity.getId().toString()); - listItem.setItem(listItemEntity.getItem()); - listItem.setPosition(listItemEntity.getPosition()); - - item.setlistItem(listItem); - - Germplasm germplasm = germplasmService.convertFromEntity(germplasmEntity); - - item.setItem(germplasm); - - listItems.add(item); - } - - return listItems; + return germplasmService.searchGermplasmByList(list, searchRequest, metadata); } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepository.java b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepository.java index 732924f5..98ae8f25 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepository.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepository.java @@ -5,6 +5,7 @@ import java.util.Optional; import java.util.UUID; +import jakarta.persistence.Tuple; import org.brapi.test.BrAPITestServer.exceptions.InvalidPagingException; import org.brapi.test.BrAPITestServer.model.entity.BrAPIPrimaryEntity; import org.brapi.test.BrAPITestServer.service.SearchQueryBuilder; @@ -34,5 +35,5 @@ public interface BrAPIRepository findByIdIn(List ids); - List testMappedEntityHql(SearchQueryBuilder searchQuery); + public Page findAllBySearchAndPaginateMultipleEntitySelect(SearchQueryBuilder searchQuery, Pageable pageReq) throws InvalidPagingException; } \ No newline at end of file diff --git a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java index 30a4fdfb..1455d0e9 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java @@ -9,6 +9,7 @@ import java.util.stream.Collectors; import jakarta.persistence.EntityManager; +import jakarta.persistence.Tuple; import jakarta.persistence.TypedQuery; import org.brapi.test.BrAPITestServer.exceptions.InvalidPagingException; @@ -59,6 +60,23 @@ public Page findAllBySearchAndPaginate(SearchQueryBuilder searchQuery, Pag return new PageImpl<>(content, pageReq, totalCount); } + public Page findAllBySearchAndPaginateMultipleEntitySelect(SearchQueryBuilder searchQuery, Pageable pageReq) throws InvalidPagingException { + applyUserId(searchQuery); + + Long totalCount = getTotalCount(searchQuery); + + validatePageNumber(pageReq, totalCount); + + if (totalCount == 0) { + // Short-circuit to avoid running possible long-running query + return new PageImpl<>(Collections.emptyList(), pageReq, totalCount); + } + + List content = getPagedContentMultipleEntitySelect(searchQuery, pageReq); + + return new PageImpl<>(content, pageReq, totalCount); + } + /** * This method should be used when there is a need to page entities that are complex and have lots of lazily loaded, * one-to-many collection attributes. Call this method with the query you've built. @@ -181,6 +199,17 @@ private List getPagedContent(SearchQueryBuilder searchQuery, Pageable page return query.getResultList(); } + private List getPagedContentMultipleEntitySelect(SearchQueryBuilder searchQuery, Pageable pageReq) { + TypedQuery query = entityManager.createQuery(searchQuery.getQuery(), Tuple.class); + + setQueryParamsMultipleEntitySelect(query, searchQuery); + + query.setFirstResult((int) pageReq.getOffset()); + query.setMaxResults(pageReq.getPageSize()); + + return query.getResultList(); + } + private List searchEntitiesWithIds(SearchQueryBuilder searchQuery, List ids) { searchQuery.appendList(ids.stream().map(Object::toString).collect(Collectors.toList()), "id"); @@ -205,6 +234,12 @@ private void setQueryParams(TypedQuery query, SearchQueryBuilder searchQue } } + private void setQueryParamsMultipleEntitySelect(TypedQuery query, SearchQueryBuilder searchQuery) { + for (Entry entry : searchQuery.getParams().entrySet()) { + query.setParameter(entry.getKey(), entry.getValue()); + } + } + private List getPagedContentIdsOnly(SearchQueryBuilder searchQuery, Pageable pageReq) { TypedQuery query = entityManager.createQuery(searchQuery.getIdQuery(), UUID.class); @@ -248,17 +283,4 @@ private void validatePageNumber(Pageable pageReq, Long totalCount) throws Invali throw new InvalidPagingException(); } } - - public List testMappedEntityHql(SearchQueryBuilder searchQuery) { - searchQuery.findGermplasmListItems(); - TypedQuery query = entityManager.createQuery(searchQuery.testHql, Object[].class); - - for (Entry entry : searchQuery.getParams().entrySet()) { - query.setParameter(entry.getKey(), entry.getValue()); - } - - query.setFirstResult((int) 0); - query.setMaxResults(10); - return query.getResultList(); - } } 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 2d34a853..843d4721 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -8,7 +8,6 @@ import io.swagger.model.FilterBy; import io.swagger.model.GeoJSONSearchArea; -import io.swagger.model.core.ListTypes; import io.swagger.model.sort.SortBy; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; @@ -18,8 +17,11 @@ public class SearchQueryBuilder { private String selectClause; + private String selectMultipleEntities; private String selectOnlyIds; private String selectOnlyIdsSubquery; + private String joinClause; + private String joinClauseIdSubquery; private String whereClause; private String defaultSort; private String sortClause; @@ -30,17 +32,19 @@ public class SearchQueryBuilder { private final String default_date = "1970-01-01"; public String testHql; - public SearchQueryBuilder(Class clazz) { - this.selectClause = "SELECT distinct entity FROM " + clazz.getSimpleName() + " entity "; + public SearchQueryBuilder(Class baseEntity) { + this.selectClause = "SELECT distinct entity FROM " + baseEntity.getSimpleName() + " entity "; // This is the top level query for selectOnlyIds. It is used to construct the full id query in getIdQuery() - this.selectOnlyIds = "SELECT entity.id FROM " + clazz.getSimpleName() + " entity "; + this.selectOnlyIds = "SELECT entity.id FROM " + baseEntity.getSimpleName() + " entity "; // This subquery will contain all the joins and filters necessary for the selectOnlyIds query and protects against duplicates using distinct - this.selectOnlyIdsSubquery = "SELECT distinct entity2.id FROM " + clazz.getSimpleName() + " entity2 "; + this.selectOnlyIdsSubquery = "SELECT distinct entity2.id FROM " + baseEntity.getSimpleName() + " entity2 "; + this.joinClause = ""; + this.joinClauseIdSubquery = ""; this.whereClause = "WHERE 1=1 "; this.defaultSort = " ORDER BY entity.id ASC "; this.sortClause = ""; this.params = new HashMap<>(); - this.clazz = clazz; + this.clazz = baseEntity; } public String getQuery() { @@ -49,7 +53,7 @@ public String getQuery() { sortClause = defaultSort; } - return selectClause + whereClause + sortClause; + return selectClause + joinClause + whereClause + sortClause; } /** @@ -88,7 +92,7 @@ public String getIdQuery() { String entityRegex = "(? withExRefs(List exRefIds, List exRe public SearchQueryBuilder join(String join, String name) { if (!this.joinedTables.contains(join) && !this.joinedFetchedTables.contains(join)) { - this.selectClause += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; - this.selectOnlyIdsSubquery += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; + this.joinClause += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; + this.joinClauseIdSubquery += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; this.joinedTables.add(join); } else if (joinedFetchedTables.contains(join) && !this.joinedTables.contains(join)) { - this.selectOnlyIdsSubquery += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; + this.joinClauseIdSubquery += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; } return this; } @@ -358,11 +362,11 @@ public SearchQueryBuilder leftJoinFetch(String join, String name, boolean ove 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.joinClause = this.joinClause.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.joinClause += generateLeftJoinFetch(join, name); this.joinedFetchedTables.add(join); } return this; @@ -377,8 +381,8 @@ public SearchQueryBuilder removeAndReplaceLeftJoinFetch(String join, String existingJoin, String existingName) { - this.selectClause = - this.selectClause.replace(generateLeftJoinFetch(existingJoin, existingName), generateLeftJoinFetch(join, name)); + this.joinClause = + this.joinClause.replace(generateLeftJoinFetch(existingJoin, existingName), generateLeftJoinFetch(join, name)); this.joinedFetchedTables.remove(existingJoin); this.joinedFetchedTables.add(join); @@ -391,8 +395,8 @@ public SearchQueryBuilder removeAndReplaceLeftJoinFetch(String join, * base query criteria to add another join fetch with leftJoinFetch() */ public SearchQueryBuilder removeLeftJoinFetch(String join, String name) { - this.selectClause = - this.selectClause.replace(generateLeftJoinFetch(join, name), ""); + this.joinClause = + this.joinClause.replace(generateLeftJoinFetch(join, name), ""); this.joinedFetchedTables.remove(join); return this; } @@ -530,28 +534,4 @@ private void joinCollectionColumn(String submittedSortFilterColumnName) { this.join(joinTableName, joinTableName); } - - public SearchQueryBuilder findGermplasmListItems() { - - testHql = """ - SELECT listItem, germplasm - FROM ListItemEntity listItem - JOIN listItem.list list - JOIN GermplasmEntity germplasm - ON germplasm.germplasmName = listItem.item - JOIN germplasm.pedigree pedigree - WHERE list.id = :listDbId - AND list.listType = :listType - AND germplasm.defaultDisplayName LIKE :defaultDisplayName - AND to_char(germplasm.createdDate, 'YYYY-MM-DD') LIKE :createdDate - ORDER BY germplasm.accessionNumber ASC, listItem.id ASC - """; - - params.put("listDbId", UUID.fromString("d200d581-1136-41e0-820d-806bf9cb63a8")); - params.put("listType", ListTypes.GERMPLASM); - params.put("defaultDisplayName", "%" + "86-MO-1-1.1" + "%"); - params.put("createdDate", "%20%"); - - return this; - } } 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 33510a5d..acbb2e54 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 @@ -7,8 +7,8 @@ import io.swagger.model.core.GermplasmListSearchRequest; import io.swagger.model.core.ListDetails; import io.swagger.model.core.ListItemWithFullItem; -import io.swagger.model.core.ProgramSearchRequest; import io.swagger.model.germ.*; +import jakarta.persistence.Tuple; import jakarta.validation.Valid; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerDbIdNotFoundException; @@ -33,7 +33,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; @@ -458,7 +457,7 @@ private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder> searchGermplasmByList(ListDetails list, GermplasmListSearchRequest request) { + public List> searchGermplasmByList(ListDetails list, GermplasmListSearchRequest request, Metadata metadata) { return new ArrayList<>(); } @@ -1121,11 +1120,23 @@ private List getRedundantInstitutes( return dupInstitutes; } - public List getGermplasmInList() { - - SearchQueryBuilder searchQuery = new SearchQueryBuilder<>( - GermplasmEntity.class); - return germplasmRepository.testMappedEntityHql(searchQuery); - } +// public List searchGermplasmByList(GermplasmListSearchRequest searchRequest, Metadata metadata) throws BrAPIServerException { +// +// Pageable pageReq = PagingUtility.getPageRequest(metadata); +// +// SearchQueryBuilder searchQuery = buildGermplasmSearchQuery(searchRequest); +// +// Page listItemAndGerm = germplasmRepository.findAllBySearchAndPaginateMultipleEntitySelect(searchQuery, pageReq); +// +// if (!listItemAndGerm.isEmpty()) { +// // Extract germs from page object, pass here +// // Ensure search query searches on germplasm and joins on list by id and type to get same results as above +// fetchRemainingGermCollectionsUsingQuery(searchQuery, listItemAndGerm); +// } +// +// SearchQueryBuilder searchQuery = new SearchQueryBuilder<>( +// GermplasmEntity.class); +// return germplasmRepository.testMappedEntityHql(searchQuery); +// } } From 87f96081b325068d2e76e9f9a784cab455e8aeb9 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Wed, 14 Oct 2026 18:34:03 -0400 Subject: [PATCH 42/56] Get /lists/{listDbId}/search/items into fully functional state --- .../java/io/swagger/model/SearchRequest.java | 18 +-- .../core/GermplasmListSearchRequest.java | 10 +- .../java/io/swagger/model/core/ListItem.java | 5 - .../model/core/ListItemWithFullItem.java | 4 +- .../model/core/StudySearchRequest.java | 38 ++--- .../model/core/TrialSearchRequest.java | 26 +-- .../model/germ/GermplasmSearchRequest.java | 30 ++-- .../GermplasmListSearchRequestHandler.java | 6 +- .../model/dto/EntityColumnNameAndType.java | 28 ---- .../dto/SortAndFilterEntityColumnInfo.java | 48 ++++++ .../primaryEntities/BrAPIRepositoryImpl.java | 16 +- .../service/SearchQueryBuilder.java | 74 ++++++--- .../service/core/ListItemService.java | 1 + .../service/core/StudyService.java | 4 +- .../service/core/TrialService.java | 4 +- .../service/germ/GermplasmService.java | 150 ++++++++++++++---- 16 files changed, 307 insertions(+), 155 deletions(-) delete mode 100644 src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityColumnNameAndType.java create mode 100644 src/main/java/org/brapi/test/BrAPITestServer/model/dto/SortAndFilterEntityColumnInfo.java diff --git a/src/main/java/io/swagger/model/SearchRequest.java b/src/main/java/io/swagger/model/SearchRequest.java index b59090fe..7999746f 100644 --- a/src/main/java/io/swagger/model/SearchRequest.java +++ b/src/main/java/io/swagger/model/SearchRequest.java @@ -4,7 +4,7 @@ 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.SortAndFilterEntityColumnInfo; import org.brapi.test.BrAPITestServer.model.dto.EntityType; import org.springframework.http.HttpStatus; @@ -150,7 +150,7 @@ public void setFilterBy(List filterBy) throws BrAPIServerException { return; } - Map allowedSortFilterNames = getEntityColAndTypeBySubmittedNameMap(); + Map allowedSortFilterNames = getEntityColInfoBySubmittedNameMap(); for (FilterBy filterByItem : filterBy) { @@ -162,17 +162,17 @@ public void setFilterBy(List filterBy) throws BrAPIServerException { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "value attribute not provided in element of filterBy list."); } - EntityColumnNameAndType entityColumnNameAndType = allowedSortFilterNames.get(filterByItem.getFilterOn()); + SortAndFilterEntityColumnInfo sortAndFilterEntityColumnInfo = allowedSortFilterNames.get(filterByItem.getFilterOn()); - if (entityColumnNameAndType == null) { + if (sortAndFilterEntityColumnInfo == 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) { + if (sortAndFilterEntityColumnInfo.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)); + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, String.format("Filtering not implemented for column name [%s] with associated data type [%s]", sortAndFilterEntityColumnInfo.getEntityColumnName(), EntityType.BOOLEAN)); } } this.filterBy = filterBy; @@ -188,14 +188,14 @@ public void setSortBy(List sortBy) throws BrAPIServerException { return; } - Map allowedSortFilterNames = getEntityColAndTypeBySubmittedNameMap(); + Map allowedSortFilterNames = getEntityColInfoBySubmittedNameMap(); 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()); + SortAndFilterEntityColumnInfo sortColumnEntityNameAndType = getEntityColInfoBySubmittedNameMap().get(sortByItem.getSortedOn()); if (sortColumnEntityNameAndType == null) { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, @@ -208,7 +208,7 @@ public void setSortBy(List sortBy) throws BrAPIServerException { } @JsonIgnore - public Map getEntityColAndTypeBySubmittedNameMap() throws BrAPIServerException { + public Map getEntityColInfoBySubmittedNameMap() 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/GermplasmListSearchRequest.java b/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java index 2d50ac7c..b578b30c 100644 --- a/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java +++ b/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java @@ -1,7 +1,7 @@ package io.swagger.model.core; import io.swagger.model.germ.GermplasmSearchRequest; -import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; +import org.brapi.test.BrAPITestServer.model.dto.SortAndFilterEntityColumnInfo; import org.brapi.test.BrAPITestServer.model.dto.EntityType; import java.util.Collections; @@ -11,23 +11,23 @@ public class GermplasmListSearchRequest extends GermplasmSearchRequest { // The only real difference in this request is that there's an extra sort/filter field that GermplasmSearchRequest doesn't have. // It is specific to lists. - private static final Map + private static final Map LIST_ALLOWED_FIELDS; static { - Map fields = + Map fields = copyAllowedSortAndFilterFields(); fields.put( "listItemPosition", - new EntityColumnNameAndType("listItem.position", EntityType.NUMBER) + new SortAndFilterEntityColumnInfo("listItem.position", EntityType.NUMBER) ); LIST_ALLOWED_FIELDS = Collections.unmodifiableMap(fields); } @Override - public Map getEntityColAndTypeBySubmittedNameMap() { + public Map getEntityColInfoBySubmittedNameMap() { return LIST_ALLOWED_FIELDS; } diff --git a/src/main/java/io/swagger/model/core/ListItem.java b/src/main/java/io/swagger/model/core/ListItem.java index 0627eb7b..a3f70dd2 100644 --- a/src/main/java/io/swagger/model/core/ListItem.java +++ b/src/main/java/io/swagger/model/core/ListItem.java @@ -21,11 +21,6 @@ public class ListItem { @JsonProperty("item") private String item = null; - public ListItem listDbId(String listDbId) { - this.listItemDbId = listDbId; - return this; - } - /** * The unique identifier for a List * @return listDbId diff --git a/src/main/java/io/swagger/model/core/ListItemWithFullItem.java b/src/main/java/io/swagger/model/core/ListItemWithFullItem.java index bacfb5bf..322b43ef 100644 --- a/src/main/java/io/swagger/model/core/ListItemWithFullItem.java +++ b/src/main/java/io/swagger/model/core/ListItemWithFullItem.java @@ -24,11 +24,11 @@ public class ListItemWithFullItem { @ApiModelProperty(example = "6f621cfa", required = true, value = "The unique identifier for a List") - public ListItem getlistItem() { + public ListItem getListItem() { return listItem; } - public void setlistItem(ListItem listItem) { + public void setListItem(ListItem listItem) { this.listItem = listItem; } diff --git a/src/main/java/io/swagger/model/core/StudySearchRequest.java b/src/main/java/io/swagger/model/core/StudySearchRequest.java index ce9b0376..26d5f223 100644 --- a/src/main/java/io/swagger/model/core/StudySearchRequest.java +++ b/src/main/java/io/swagger/model/core/StudySearchRequest.java @@ -6,7 +6,7 @@ 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.SortAndFilterEntityColumnInfo; import org.brapi.test.BrAPITestServer.model.dto.EntityType; import java.util.ArrayList; @@ -16,24 +16,24 @@ 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 = + private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = Map.ofEntries( - Map.entry("commonCropName", new EntityColumnNameAndType("crop.cropName", EntityType.TEXT)), - Map.entry("externalReferenceId", new EntityColumnNameAndType("*externalReferences.externalReferenceId", EntityType.TEXT)), - Map.entry("externalReferenceSource", new EntityColumnNameAndType("*externalReferences.externalReferenceSource", EntityType.TEXT)), - 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("studyCode", new EntityColumnNameAndType("studyCode", EntityType.TEXT)), - Map.entry("studyDbId", new EntityColumnNameAndType("id", EntityType.UUID)), - Map.entry("studyLocation", new EntityColumnNameAndType("location.id", EntityType.UUID)), - Map.entry("studyPUI", new EntityColumnNameAndType("studyPUI", EntityType.TEXT)), - Map.entry("trialDbId", new EntityColumnNameAndType("trial.id", EntityType.UUID)), - Map.entry("studyType", new EntityColumnNameAndType("studyType", EntityType.TEXT)), - Map.entry("studyName", new EntityColumnNameAndType("studyName", EntityType.TEXT)) + Map.entry("commonCropName", new SortAndFilterEntityColumnInfo("crop.cropName", EntityType.TEXT)), + Map.entry("externalReferenceId", new SortAndFilterEntityColumnInfo("*externalReferences.externalReferenceId", EntityType.TEXT)), + Map.entry("externalReferenceSource", new SortAndFilterEntityColumnInfo("*externalReferences.externalReferenceSource", EntityType.TEXT)), + Map.entry("germplasmDbId", new SortAndFilterEntityColumnInfo("*obsunit.germplasm.id", EntityType.UUID)), + Map.entry("locationDbId", new SortAndFilterEntityColumnInfo("location.id", EntityType.UUID)), + Map.entry("observationVariableDbId", new SortAndFilterEntityColumnInfo("*observation.observationVariable.id", EntityType.UUID)), + Map.entry("programDbId", new SortAndFilterEntityColumnInfo("trial.program.id", EntityType.UUID)), + Map.entry("programName", new SortAndFilterEntityColumnInfo("trial.program.name", EntityType.TEXT)), + Map.entry("seasonDbId", new SortAndFilterEntityColumnInfo("*season.id", EntityType.UUID)), + Map.entry("studyCode", new SortAndFilterEntityColumnInfo("studyCode", EntityType.TEXT)), + Map.entry("studyDbId", new SortAndFilterEntityColumnInfo("id", EntityType.UUID)), + Map.entry("studyLocation", new SortAndFilterEntityColumnInfo("location.id", EntityType.UUID)), + Map.entry("studyPUI", new SortAndFilterEntityColumnInfo("studyPUI", EntityType.TEXT)), + Map.entry("trialDbId", new SortAndFilterEntityColumnInfo("trial.id", EntityType.UUID)), + Map.entry("studyType", new SortAndFilterEntityColumnInfo("studyType", EntityType.TEXT)), + Map.entry("studyName", new SortAndFilterEntityColumnInfo("studyName", EntityType.TEXT)) ); @JsonProperty("commonCropNames") @@ -586,7 +586,7 @@ public Integer getTotalParameterCount() { } @Override - public Map getEntityColAndTypeBySubmittedNameMap() { + public Map getEntityColInfoBySubmittedNameMap() { 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 2760e5d5..c4f1c293 100644 --- a/src/main/java/io/swagger/model/core/TrialSearchRequest.java +++ b/src/main/java/io/swagger/model/core/TrialSearchRequest.java @@ -4,7 +4,7 @@ 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.SortAndFilterEntityColumnInfo; import org.brapi.test.BrAPITestServer.model.dto.EntityType; import java.time.LocalDate; @@ -13,18 +13,18 @@ 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 = + 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) + "trialName", new SortAndFilterEntityColumnInfo("trialName", EntityType.TEXT), + "createdDate", new SortAndFilterEntityColumnInfo("createdDate", EntityType.TEXT), + "createdBy", new SortAndFilterEntityColumnInfo("createdBy", EntityType.TEXT), + "trialDbId", new SortAndFilterEntityColumnInfo("id", EntityType.UUID), + "programDbId",new SortAndFilterEntityColumnInfo("program.id", EntityType.UUID), + "startDate", new SortAndFilterEntityColumnInfo("startDate", EntityType.TEXT), + "endDate", new SortAndFilterEntityColumnInfo("endDate", EntityType.TEXT), + "active", new SortAndFilterEntityColumnInfo("active", EntityType.BOOLEAN), + "programName", new SortAndFilterEntityColumnInfo("program.name", EntityType.TEXT), + "locationDbId", new SortAndFilterEntityColumnInfo("*studies.location.id", EntityType.UUID) ); @JsonProperty("commonCropNames") @@ -444,7 +444,7 @@ public Integer getTotalParameterCount() { } @Override - public Map getEntityColAndTypeBySubmittedNameMap() { + public Map getEntityColInfoBySubmittedNameMap() { return ALLOWED_SORT_AND_FILTER_FIELDS; } } diff --git a/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java b/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java index 029ab807..957e7465 100644 --- a/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java +++ b/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; 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.SortAndFilterEntityColumnInfo; import org.brapi.test.BrAPITestServer.model.dto.EntityType; import java.util.*; @@ -12,23 +12,23 @@ public class GermplasmSearchRequest 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 = + private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = Map.ofEntries( // accessionNumber = GID for DeltaBreed - Map.entry("accessionNumber", new EntityColumnNameAndType("accessionNumber", EntityType.NUMBER)), - Map.entry("defaultDisplayName", new EntityColumnNameAndType("defaultDisplayName", EntityType.TEXT)), - Map.entry("breedingMethod", new EntityColumnNameAndType("methodName", EntityType.TEXT)), - Map.entry("seedSource", new EntityColumnNameAndType("seedSource", EntityType.TEXT)), - Map.entry("pedigree",new EntityColumnNameAndType("pedigree.pedigreeString", EntityType.TEXT)), - Map.entry("femaleParentGID", new EntityColumnNameAndType("femaleParentGid", EntityType.TEXT)), - Map.entry("maleParentGID", new EntityColumnNameAndType("maleParentGid", EntityType.TEXT)), - Map.entry("createdDate", new EntityColumnNameAndType("createdDate", EntityType.DATE)), - Map.entry("createdBy", new EntityColumnNameAndType("createdBy", EntityType.TEXT)), - Map.entry("importEntryNumber", new EntityColumnNameAndType("importEntryNumber", EntityType.NUMBER)), - Map.entry("synonyms", new EntityColumnNameAndType("*synonyms.synonym", EntityType.TEXT)) + Map.entry("accessionNumber", new SortAndFilterEntityColumnInfo("accessionNumber", EntityType.NUMBER)), + Map.entry("defaultDisplayName", new SortAndFilterEntityColumnInfo("defaultDisplayName", EntityType.TEXT)), + Map.entry("breedingMethod", new SortAndFilterEntityColumnInfo("methodName", EntityType.TEXT)), + Map.entry("seedSource", new SortAndFilterEntityColumnInfo("seedSource", EntityType.TEXT)), + Map.entry("pedigree",new SortAndFilterEntityColumnInfo("pedigree.pedigreeString", EntityType.TEXT)), + Map.entry("femaleParentGID", new SortAndFilterEntityColumnInfo("femaleParentGid", EntityType.TEXT)), + Map.entry("maleParentGID", new SortAndFilterEntityColumnInfo("maleParentGid", EntityType.TEXT)), + Map.entry("createdDate", new SortAndFilterEntityColumnInfo("createdDate", EntityType.DATE)), + Map.entry("createdBy", new SortAndFilterEntityColumnInfo("createdBy", EntityType.TEXT)), + Map.entry("importEntryNumber", new SortAndFilterEntityColumnInfo("*listItem.position", EntityType.NUMBER, false, true)), + Map.entry("synonyms", new SortAndFilterEntityColumnInfo("*synonyms.synonym", EntityType.TEXT, true, false)) ); - protected static Map copyAllowedSortAndFilterFields() { + protected static Map copyAllowedSortAndFilterFields() { return new HashMap<>(ALLOWED_SORT_AND_FILTER_FIELDS); } @@ -642,7 +642,7 @@ public Integer getTotalParameterCount() { } @Override - public Map getEntityColAndTypeBySubmittedNameMap() { + public Map getEntityColInfoBySubmittedNameMap() { return ALLOWED_SORT_AND_FILTER_FIELDS; } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java b/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java index 718401bb..b1d731b7 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java @@ -2,14 +2,10 @@ import io.swagger.model.Metadata; import io.swagger.model.core.*; -import io.swagger.model.germ.Germplasm; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; -import org.brapi.test.BrAPITestServer.model.entity.core.ListItemEntity; -import org.brapi.test.BrAPITestServer.model.entity.germ.GermplasmEntity; import org.brapi.test.BrAPITestServer.service.germ.GermplasmService; import org.springframework.stereotype.Component; -import java.util.ArrayList; import java.util.List; @Component @@ -36,6 +32,6 @@ public List> search( GermplasmListSearchRequest searchRequest, Metadata metadata) throws BrAPIServerException { - return germplasmService.searchGermplasmByList(list, searchRequest, metadata); + return germplasmService.findGermplasmByList(list, searchRequest, 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 deleted file mode 100644 index 71f4a9f2..00000000 --- a/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityColumnNameAndType.java +++ /dev/null @@ -1,28 +0,0 @@ -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/SortAndFilterEntityColumnInfo.java b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/SortAndFilterEntityColumnInfo.java new file mode 100644 index 00000000..8fbf43e1 --- /dev/null +++ b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/SortAndFilterEntityColumnInfo.java @@ -0,0 +1,48 @@ +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 SortAndFilterEntityColumnInfo { + private final String entityColumnName; + private final EntityType entityType; + private final boolean isCollectionOnEntity; + private final boolean isJoinOnUnrelatedEntity; + + + public SortAndFilterEntityColumnInfo(String entityColumnName, EntityType entityType) { + this.entityColumnName = entityColumnName; + this.entityType = entityType; + this.isCollectionOnEntity = false; + this.isJoinOnUnrelatedEntity = false; + } + + public SortAndFilterEntityColumnInfo(String entityColumnName, EntityType entityType, boolean isCollectionOnEntity, boolean isJoinOnUnrelatedEntity) { + this.entityColumnName = entityColumnName; + this.entityType = entityType; + this.isCollectionOnEntity = isCollectionOnEntity; + this.isJoinOnUnrelatedEntity = isJoinOnUnrelatedEntity; + } + + public String getEntityColumnName() { + return entityColumnName; + } + + public EntityType getEntityType() { + return entityType; + } + + public boolean isCollectionOnEntity() { + return isCollectionOnEntity; + } + + public boolean isJoinOnUnrelatedEntity() { + return isJoinOnUnrelatedEntity; + } + + @Override + public String toString() { + return this.entityColumnName; + } +} diff --git a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java index 1455d0e9..52d0f081 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java @@ -63,7 +63,7 @@ public Page findAllBySearchAndPaginate(SearchQueryBuilder searchQuery, Pag public Page findAllBySearchAndPaginateMultipleEntitySelect(SearchQueryBuilder searchQuery, Pageable pageReq) throws InvalidPagingException { applyUserId(searchQuery); - Long totalCount = getTotalCount(searchQuery); + Long totalCount = getMultipleSelectCountQuery(searchQuery); validatePageNumber(pageReq, totalCount); @@ -91,6 +91,7 @@ public Page findAllBySearchAndPaginateMultipleEntitySelect(SearchQueryBui * If the need is to fetch according to an entirely different base criteria, feel free to call this method again. * * The results will be paged and ordered based on the submitted searchQuery and pageReq. + * TODO: This method and it's confusing use of SearchQueryBuilder.idQuery may not be necessary at all if we keep left join fetches out of initial queries and fetch after. */ public Page findAllBySearchPaginatingWithFetches(SearchQueryBuilder searchQuery, Pageable pageReq) throws InvalidPagingException { @@ -255,6 +256,19 @@ private List getPagedContentIdsOnly(SearchQueryBuilder searchQuery, Pag } + private Long getMultipleSelectCountQuery(SearchQueryBuilder searchQuery) + { + TypedQuery query = entityManager.createQuery(searchQuery.getCountQueryMultipleSelect(), Long.class); + for (Entry entry : searchQuery.getParams().entrySet()) { + query.setParameter(entry.getKey(), entry.getValue()); + } + + List content = query.getResultList(); + if (!content.isEmpty()) { + return content.getFirst(); + } + return 0L; + } private Long getTotalCount(SearchQueryBuilder searchQuery) { 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 843d4721..bcd411ec 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -10,14 +10,13 @@ import io.swagger.model.GeoJSONSearchArea; 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.SortAndFilterEntityColumnInfo; import org.brapi.test.BrAPITestServer.model.dto.EntityType; import org.springframework.http.HttpStatus; public class SearchQueryBuilder { private String selectClause; - private String selectMultipleEntities; private String selectOnlyIds; private String selectOnlyIdsSubquery; private String joinClause; @@ -30,7 +29,6 @@ public class SearchQueryBuilder { private List joinedFetchedTables = new ArrayList<>(); private Class clazz; private final String default_date = "1970-01-01"; - public String testHql; public SearchQueryBuilder(Class baseEntity) { this.selectClause = "SELECT distinct entity FROM " + baseEntity.getSimpleName() + " entity "; @@ -56,6 +54,22 @@ public String getQuery() { return selectClause + joinClause + whereClause + sortClause; } + public String getCountQueryMultipleSelect() { + String firstSelection = selectClause + .replaceFirst("(?i)^SELECT\\s+(?:DISTINCT\\s+)?", "") + .split(",")[0] + .trim(); + + int fromIndex = selectClause.toUpperCase(Locale.ROOT).indexOf(" FROM "); + + String fromClause = selectClause.substring(fromIndex); + + return "SELECT COUNT(DISTINCT " + firstSelection + ".id) " + + fromClause + + joinClause.replaceAll("(?i)LEFT JOIN FETCH", "LEFT JOIN") + + whereClause; + } + /** * The goal of this query is to return a query that will return only the BrAPI dbIds of the source entity, applying * sorting, filtering, and pagination to the query. @@ -106,6 +120,13 @@ public Class getClazz() { return clazz; } + public SearchQueryBuilder modifySelect(List entityAliases) { + + this.selectClause = "SELECT distinct " + String.join(", ", entityAliases) + " FROM " + clazz.getSimpleName() + " entity "; + + return this; + } + public SearchQueryBuilder appendList(List list, String columnName) { String paramName = paramFilter(columnName); if (list != null && !list.isEmpty()) { @@ -353,6 +374,15 @@ public SearchQueryBuilder join(String join, String name) { return this; } + public SearchQueryBuilder joinEntity(Class joinedEntity, + String alias, + String onClause) { + this.joinClause += " JOIN " + joinedEntity.getSimpleName() + + " " + alias + + " ON " + onClause; + return this; + } + public SearchQueryBuilder leftJoinFetch(String join, String name) { return leftJoinFetch(join, name, false); } @@ -434,19 +464,19 @@ private String paramFilterPattern(String param) { * - A column name * - An order (DESC, ASC) */ - public SearchQueryBuilder sortBy(List sortBy, Map entityColAndTypeBySubmittedName) throws BrAPIServerException { + public SearchQueryBuilder sortBy(List sortBy, Map entityColInfoBySubmittedName) throws BrAPIServerException { if (sortBy == null || sortBy.isEmpty()) { return this; } for (SortBy sort : sortBy) { - // At this point, the submitted sortBy name has been verified to be in entityColAndTypeBySubmittedName - EntityColumnNameAndType entityColumnNameAndType = entityColAndTypeBySubmittedName.get(sort.getSortedOn()); + // At this point, the submitted sortBy name has been verified to be in entityColInfoBySubmittedName + SortAndFilterEntityColumnInfo sortAndFilterEntityColumnInfo = entityColInfoBySubmittedName.get(sort.getSortedOn()); - String entityColName = entityColumnNameAndType.getEntityColumnName(); + String entityColName = sortAndFilterEntityColumnInfo.getEntityColumnName(); - if (entityColName.startsWith("*")) { + if (sortAndFilterEntityColumnInfo.isCollectionOnEntity()) { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "Sorting on one to many relationships not supported"); } @@ -457,7 +487,8 @@ public SearchQueryBuilder sortBy(List sortBy, Map filterBy(List filterBy, Map entityColAndTypeBySubmittedName) throws BrAPIServerException { + public SearchQueryBuilder filterBy(List filterBy, Map entityColAndTypeBySubmittedName) throws BrAPIServerException { SearchQueryBuilder searchQuery = this; if (filterBy == null || filterBy.isEmpty()) { @@ -503,21 +534,22 @@ public SearchQueryBuilder filterBy(List filterBy, Map 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") - .sortBy(request.getSortByElements(), request.getEntityColAndTypeBySubmittedNameMap()) - .filterBy(request.getFilterBy(), request.getEntityColAndTypeBySubmittedNameMap()); + .sortBy(request.getSortByElements(), request.getEntityColInfoBySubmittedNameMap()) + .filterBy(request.getFilterBy(), request.getEntityColInfoBySubmittedNameMap()); Page studiesPage = studyRepository.findAllBySearchAndPaginate(searchQuery, pageReq); PagingUtility.calculateMetaData(metaData, studiesPage); 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 21c30c65..6c3fc54d 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 @@ -127,8 +127,8 @@ public List findTrials(@Valid TrialSearchRequest request, Metadata metada .appendList(request.getStudyNames(), "*studies.studyName").appendList(request.getTrialDbIds(), "id") .appendList(request.getTrialNames(), "trialName") .appendDateRange(request.getSearchDateRangeStart(), request.getSearchDateRangeEnd(), "startDate") - .sortBy(request.getSortByElements(), request.getEntityColAndTypeBySubmittedNameMap()) - .filterBy(request.getFilterBy(), request.getEntityColAndTypeBySubmittedNameMap()); + .sortBy(request.getSortByElements(), request.getEntityColInfoBySubmittedNameMap()) + .filterBy(request.getFilterBy(), request.getEntityColInfoBySubmittedNameMap()); Page trialsPage = trialRepository.findAllBySearchAndPaginate(searchQuery, pageReq); PagingUtility.calculateMetaData(metadata, trialsPage); 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 acbb2e54..eea70c74 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 @@ -6,8 +6,11 @@ import io.swagger.model.core.GermplasmListSearchRequest; import io.swagger.model.core.ListDetails; +import io.swagger.model.core.ListItem; import io.swagger.model.core.ListItemWithFullItem; import io.swagger.model.germ.*; +import io.swagger.model.sort.SortBy; +import io.swagger.model.sort.SortOrder; import jakarta.persistence.Tuple; import jakarta.validation.Valid; @@ -16,6 +19,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.ListItemEntity; 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; @@ -140,6 +144,8 @@ public Page findGermplasmEntities(@Valid GermplasmSearchRequest if (!germs.isEmpty()) { // If records were found, iterate through the rest of the lazily loaded collections of the GermplasmEntity // and LEFT JOIN FETCH all of them. + log.debug("Fetching synonyms"); + fetchSynonyms(ids, germs); log.debug("Fetching xrefs"); fetchXrefs(ids, germs); log.debug("fetching attributes"); @@ -175,8 +181,7 @@ public List findGermplasmEntitiesWithoutPaging(@Valid Germplasm private SearchQueryBuilder buildGermplasmSearchQuery(GermplasmSearchRequest request) throws BrAPIServerException { SearchQueryBuilder searchQuery = new SearchQueryBuilder( GermplasmEntity.class); - searchQuery.leftJoinFetch("synonyms", "synonyms") - .leftJoinFetch("breedingMethod", "breedingMethod") + searchQuery.leftJoinFetch("breedingMethod", "breedingMethod") .leftJoinFetch("crop", "crop") .leftJoinFetch("pedigree", "pedigree") .leftJoinFetch("*pedigree.crossingProject", "crossingProject"); @@ -210,8 +215,8 @@ private SearchQueryBuilder buildGermplasmSearchQuery(GermplasmS .appendList(request.getGenus(), "genus").appendList(request.getSpecies(), "species") .appendNamesList(request.getBinomialNames(), "genus", "genus", "species") .appendList(request.getFamilyCodes(), "familyCode") - .filterBy(request.getFilterBy(), request.getEntityColAndTypeBySubmittedNameMap()) - .sortBy(request.getSortByElements(), request.getEntityColAndTypeBySubmittedNameMap()); + .filterBy(request.getFilterBy(), request.getEntityColInfoBySubmittedNameMap()) + .sortBy(request.getSortByElements(), request.getEntityColInfoBySubmittedNameMap()); return searchQuery; } @@ -220,6 +225,21 @@ private SearchQueryBuilder buildGermplasmSearchQuery(GermplasmS // TODO: Therefore no need to store a map and can just iterate through both at the same time O(n). // TODO: Revisit when sortBy is added to GermplasmSearchRequest and we can pass through a sortBy value. + + private void fetchSynonyms(List ids, Page germEntities) { + SearchQueryBuilder searchQuery = new SearchQueryBuilder<>(GermplasmEntity.class); + + searchQuery.leftJoinFetch("synonyms", "synonyms") + .appendIds(ids); + + List germsWithSynonyms = germplasmRepository.findAllBySearch(searchQuery); + + Map> synonymsByEntity = new HashMap<>(); + germsWithSynonyms.forEach(entity -> synonymsByEntity.put(entity.getId().toString(), entity.getSynonyms())); + + germEntities.forEach(entity -> entity.setSynonyms(synonymsByEntity.get(entity.getId().toString()))); + } + private void fetchXrefs(List ids, Page germEntities) { SearchQueryBuilder searchQuery = new SearchQueryBuilder<>(GermplasmEntity.class); @@ -339,16 +359,30 @@ private void fetchPedigreeEdges(List ids, Page germEntiti private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder searchQuery, List germEntities) { // Remove unused left join fetches from the base query - searchQuery.removeLeftJoinFetch("synonyms", "synonyms") - .removeLeftJoinFetch("breedingMethod", "breedingMethod") + searchQuery.removeLeftJoinFetch("breedingMethod", "breedingMethod") .removeLeftJoinFetch("crop", "crop") .removeLeftJoinFetch("pedigree", "pedigree") .removeLeftJoinFetch("*pedigree.crossingProject", "crossingProject") .resetSortClause(); + + // Fetch synonyms + log.debug("fetching synonyms"); + searchQuery.leftJoinFetch("synonyms", + "synonyms"); + + List synonyms = germplasmRepository.findAllBySearch(searchQuery); + + Map> synonymsByEntity = new HashMap<>(); + synonyms.forEach(entity -> synonymsByEntity.put(entity.getId().toString(), entity.getExternalReferences())); + + germEntities.forEach(entity -> entity.setExternalReferences(synonymsByEntity.get(entity.getId().toString()))); + // Fetch xrefs log.debug("Fetching xrefs"); - searchQuery.leftJoinFetch("externalReferences", - "externalReferences"); + searchQuery.removeAndReplaceLeftJoinFetch("externalReferences", + "externalReferences", + "synonyms", + "synonyms"); List xrefs = germplasmRepository.findAllBySearch(searchQuery); @@ -457,8 +491,14 @@ private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder> searchGermplasmByList(ListDetails list, GermplasmListSearchRequest request, Metadata metadata) { - return new ArrayList<>(); + public List> findGermplasmByList(ListDetails list, GermplasmListSearchRequest request, Metadata metadata) throws BrAPIServerException{ + + Page page = searchGermplasmByList(list, request, metadata); + + List> listItems = page.map(this::convertFromEntity).getContent(); + PagingUtility.calculateMetaData(metadata, page); + + return listItems; } public Germplasm getGermplasm(String germplasmDbId) throws BrAPIServerException { @@ -526,6 +566,23 @@ public List saveGermplasm(@Valid List body) thro .collect(Collectors.toList()); } + private ListItemWithFullItem convertFromEntity(Tuple listItemAndGermplasmEntity) { + ListItemWithFullItem listItemWithFullItem = new ListItemWithFullItem<>(); + + ListItem listItem = new ListItem(); + + ListItemEntity listItemEntity = listItemAndGermplasmEntity.get(0, ListItemEntity.class); + + listItem.setListItemDbId(listItemEntity.getId().toString()); + listItem.setPosition(listItemEntity.getPosition()); + listItem.setItem(listItemEntity.getItem()); + + listItemWithFullItem.setListItem(listItem); + listItemWithFullItem.setItem(convertFromEntity(listItemAndGermplasmEntity.get(1, GermplasmEntity.class))); + + return listItemWithFullItem; + } + public Germplasm convertFromEntity(GermplasmEntity entity) { Germplasm germ = new Germplasm(); UpdateUtility.convertFromEntity(entity, germ); @@ -1120,23 +1177,60 @@ private List getRedundantInstitutes( return dupInstitutes; } -// public List searchGermplasmByList(GermplasmListSearchRequest searchRequest, Metadata metadata) throws BrAPIServerException { -// -// Pageable pageReq = PagingUtility.getPageRequest(metadata); -// -// SearchQueryBuilder searchQuery = buildGermplasmSearchQuery(searchRequest); -// -// Page listItemAndGerm = germplasmRepository.findAllBySearchAndPaginateMultipleEntitySelect(searchQuery, pageReq); -// -// if (!listItemAndGerm.isEmpty()) { -// // Extract germs from page object, pass here -// // Ensure search query searches on germplasm and joins on list by id and type to get same results as above -// fetchRemainingGermCollectionsUsingQuery(searchQuery, listItemAndGerm); -// } -// -// SearchQueryBuilder searchQuery = new SearchQueryBuilder<>( -// GermplasmEntity.class); -// return germplasmRepository.testMappedEntityHql(searchQuery); -// } + public Page searchGermplasmByList(ListDetails list, GermplasmListSearchRequest searchRequest, Metadata metadata) throws BrAPIServerException { + + Pageable pageReq = PagingUtility.getPageRequest(metadata); + + List sortByElements = searchRequest.getSortByElements(); + + if (sortByElements == null || sortByElements.isEmpty()) { + // If there is no specified sort, sort by the position of the list. + SortBy defaultListItemSort = new SortBy("importEntryNumber", SortOrder.ASC); + searchRequest.setSortBy(List.of(defaultListItemSort)); + } + + SearchQueryBuilder searchQuery = buildGermplasmSearchQuery(searchRequest); + + searchQuery.modifySelect(List.of("listItem", "entity")); + + searchQuery.joinEntity(ListItemEntity.class, "listItem", " entity.germplasmName = listItem.item ") + .join("*listItem.list", "list") + .appendSingle(UUID.fromString(list.getListDbId()), "*list.id") + .appendEnum(list.getListType(), "*list.listType"); + + Page listItemAndGerm = germplasmRepository.findAllBySearchAndPaginateMultipleEntitySelect(searchQuery, pageReq); + + Page germs = listItemAndGerm.map( + // Germplasm is the second column of the Tuple + row -> (GermplasmEntity) row.get(1) + ); + + List ids = germs.get().map(GermplasmEntity::getId).toList(); + + if (!germs.isEmpty()) { + // If records were found, iterate through the rest of the lazily loaded collections of the GermplasmEntity + // and LEFT JOIN FETCH all of them. + log.debug("Fetching synonyms"); + fetchSynonyms(ids, germs); + log.debug("Fetching xrefs"); + fetchXrefs(ids, germs); + log.debug("fetching attributes"); + fetchAttributes(ids, germs); + log.debug("fetching donors"); + fetchDonors(ids, germs); + log.debug("fetching origins"); + fetchOrigin(ids, germs); + log.debug("fetching institutes"); + fetchInstitutes(ids, germs); + log.debug("fetching taxons"); + fetchTaxons(ids, germs); + log.debug("fetching storage codes"); + fetchStorageCodes(ids, germs); + log.debug("fetching pedigree edges"); + fetchPedigreeEdges(ids, germs); + } + + return listItemAndGerm; + } } From 3c08de58b65e54f65a4933ccd3e98afeded6e3bc Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Wed, 14 Oct 2026 18:56:17 -0400 Subject: [PATCH 43/56] Add @Formula for checking item position with 1 index --- .../model/core/GermplasmListSearchRequest.java | 2 +- .../model/entity/core/ListItemEntity.java | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java b/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java index b578b30c..6a1aeae8 100644 --- a/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java +++ b/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java @@ -20,7 +20,7 @@ public class GermplasmListSearchRequest extends GermplasmSearchRequest { fields.put( "listItemPosition", - new SortAndFilterEntityColumnInfo("listItem.position", EntityType.NUMBER) + new SortAndFilterEntityColumnInfo("*listItem.positionIndexedBy1", EntityType.NUMBER, false, true) ); LIST_ALLOWED_FIELDS = Collections.unmodifiableMap(fields); diff --git a/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/ListItemEntity.java b/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/ListItemEntity.java index 92b76da3..3b553823 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/ListItemEntity.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/ListItemEntity.java @@ -3,6 +3,7 @@ import jakarta.persistence.*; import org.brapi.test.BrAPITestServer.model.entity.BrAPIBaseEntity; +import org.hibernate.annotations.Formula; import org.hibernate.annotations.Where; @Entity @@ -17,6 +18,8 @@ public class ListItemEntity extends BrAPIBaseEntity { private Integer position; @Column(name = "soft_deleted") private boolean softDeleted; + @Formula("position + 1") + private Integer positionIndexedBy1; public ListEntity getList() { return list; @@ -38,4 +41,16 @@ public void setPosition(Integer position) { } public boolean getSoftDeleted() { return softDeleted; } public void setSoftDeleted(boolean softDeleted) { this.softDeleted = softDeleted; } + + public boolean isSoftDeleted() { + return softDeleted; + } + + public Integer getPositionIndexedBy1() { + return positionIndexedBy1; + } + + public void setPositionIndexedBy1(Integer positionIndexedBy1) { + this.positionIndexedBy1 = positionIndexedBy1; + } } From 6b906c9f6b5626b295f201e054f60a23a39ed798 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Wed, 14 Oct 2026 19:01:16 -0400 Subject: [PATCH 44/56] Fix GermplasmService redline in IDE not causing compile or runtime errors --- .../test/BrAPITestServer/service/germ/GermplasmService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 eea70c74..0f786abb 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 @@ -495,10 +495,10 @@ public List> findGermplasmByList(ListDetails list, Germp Page page = searchGermplasmByList(list, request, metadata); - List> listItems = page.map(this::convertFromEntity).getContent(); + Page> listItems = page.map(this::convertFromEntity); PagingUtility.calculateMetaData(metadata, page); - return listItems; + return listItems.getContent(); } public Germplasm getGermplasm(String germplasmDbId) throws BrAPIServerException { From bb71d95a992c02bca41f0778c06e6d9a064276f0 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Thu, 15 Oct 2026 18:43:38 -0400 Subject: [PATCH 45/56] Model edits --- .../core/GermplasmListSearchRequest.java | 2 -- .../model/core/ListItemSearchRequest.java | 20 ------------------- .../model/core/ListItemWithFullItem.java | 2 +- .../model/core/ListItemsListResponse.java | 12 +++++------ .../core/ListItemsListResponseResult.java | 6 +++--- 5 files changed, 10 insertions(+), 32 deletions(-) delete mode 100644 src/main/java/io/swagger/model/core/ListItemSearchRequest.java diff --git a/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java b/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java index 6a1aeae8..2ece7db8 100644 --- a/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java +++ b/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java @@ -30,6 +30,4 @@ public class GermplasmListSearchRequest extends GermplasmSearchRequest { public Map getEntityColInfoBySubmittedNameMap() { return LIST_ALLOWED_FIELDS; } - - } diff --git a/src/main/java/io/swagger/model/core/ListItemSearchRequest.java b/src/main/java/io/swagger/model/core/ListItemSearchRequest.java deleted file mode 100644 index c5fe9b33..00000000 --- a/src/main/java/io/swagger/model/core/ListItemSearchRequest.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.swagger.model.core; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.JsonNode; - -public class ListItemSearchRequest { - - @JsonProperty("searchRequest") - private JsonNode searchRequest; - - // List-item-specific filters and sorting can go here. - - public JsonNode getSearchRequest() { - return searchRequest; - } - - public void setSearchRequest(JsonNode searchRequest) { - this.searchRequest = searchRequest; - } -} diff --git a/src/main/java/io/swagger/model/core/ListItemWithFullItem.java b/src/main/java/io/swagger/model/core/ListItemWithFullItem.java index 322b43ef..f091c0a1 100644 --- a/src/main/java/io/swagger/model/core/ListItemWithFullItem.java +++ b/src/main/java/io/swagger/model/core/ListItemWithFullItem.java @@ -48,7 +48,7 @@ public boolean equals(java.lang.Object o) { if (o == null || getClass() != o.getClass()) { return false; } - ListItemWithFullItem listItemWithFullItem = (ListItemWithFullItem) o; + ListItemWithFullItem listItemWithFullItem = (ListItemWithFullItem) o; return Objects.equals(this.listItem, listItemWithFullItem.listItem) && super.equals(o); } diff --git a/src/main/java/io/swagger/model/core/ListItemsListResponse.java b/src/main/java/io/swagger/model/core/ListItemsListResponse.java index e36f43e7..a5021c38 100644 --- a/src/main/java/io/swagger/model/core/ListItemsListResponse.java +++ b/src/main/java/io/swagger/model/core/ListItemsListResponse.java @@ -15,7 +15,7 @@ */ @Validated @javax.annotation.processing.Generated(value = "io.swagger.codegen.v3.generators.java.SpringCodegen", date = "2020-03-20T16:31:52.030Z[GMT]") -public class ListItemsListResponse implements BrAPIResponse { +public class ListItemsListResponse implements BrAPIResponse> { @JsonProperty("@context") private Context _atContext = null; @@ -23,7 +23,7 @@ public class ListItemsListResponse implements BrAPIResponse result = null; public ListItemsListResponse() { this._atContext = new Context(); @@ -56,7 +56,7 @@ public void setMetadata(Metadata metadata) { this.metadata = metadata; } - public ListItemsListResponse result(ListItemsListResponseResult result) { + public ListItemsListResponse result(ListItemsListResponseResult result) { this.result = result; return this; } @@ -70,11 +70,11 @@ public ListItemsListResponse result(ListItemsListResponseResult result) { @Valid - public ListItemsListResponseResult getResult() { + public ListItemsListResponseResult getResult() { return result; } - public void setResult(ListItemsListResponseResult result) { + public void setResult(ListItemsListResponseResult result) { this.result = result; } @@ -86,7 +86,7 @@ public boolean equals(java.lang.Object o) { if (o == null || getClass() != o.getClass()) { return false; } - ListItemsListResponse listItemsListResponse = (ListItemsListResponse) o; + ListItemsListResponse listItemsListResponse = (ListItemsListResponse) o; return Objects.equals(this._atContext, listItemsListResponse._atContext) && Objects.equals(this.metadata, listItemsListResponse.metadata) && Objects.equals(this.result, listItemsListResponse.result); diff --git a/src/main/java/io/swagger/model/core/ListItemsListResponseResult.java b/src/main/java/io/swagger/model/core/ListItemsListResponseResult.java index 6a5f6561..892a2dba 100644 --- a/src/main/java/io/swagger/model/core/ListItemsListResponseResult.java +++ b/src/main/java/io/swagger/model/core/ListItemsListResponseResult.java @@ -20,12 +20,12 @@ public class ListItemsListResponseResult implements BrAPIResponseResult> data = new ArrayList>(); - public ListItemsListResponseResult data(List> data) { + public ListItemsListResponseResult data(List> data) { this.data = data; return this; } - public ListItemsListResponseResult addDataItem(ListItemWithFullItem dataItem) { + public ListItemsListResponseResult addDataItem(ListItemWithFullItem dataItem) { this.data.add(dataItem); return this; } @@ -54,7 +54,7 @@ public boolean equals(java.lang.Object o) { if (o == null || getClass() != o.getClass()) { return false; } - ListItemsListResponseResult listItemsListResponseResult = (ListItemsListResponseResult) o; + ListItemsListResponseResult listItemsListResponseResult = (ListItemsListResponseResult) o; return Objects.equals(this.data, listItemsListResponseResult.data); } From 4452596eab374c80504ef850b5c1ede3ec77e43e Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Fri, 16 Oct 2026 14:55:17 -0400 Subject: [PATCH 46/56] Force type deserialization on client, changes to support this - Addtional changes to processGermplasmForDisplay that was not occuring before. This was breaking features in the Lists tab --- .../io/swagger/model/core/ListItemsListResponse.java | 11 +++++++++++ .../controller/core/ListsApiController.java | 9 +++++++-- .../BrAPITestServer/service/core/ListItemService.java | 4 +--- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/swagger/model/core/ListItemsListResponse.java b/src/main/java/io/swagger/model/core/ListItemsListResponse.java index a5021c38..eb9c80d0 100644 --- a/src/main/java/io/swagger/model/core/ListItemsListResponse.java +++ b/src/main/java/io/swagger/model/core/ListItemsListResponse.java @@ -22,6 +22,9 @@ public class ListItemsListResponse implements BrAPIResponse result = null; @@ -78,6 +81,14 @@ public void setResult(ListItemsListResponseResult result) { this.result = result; } + public ListTypes getResultListType() { + return resultListType; + } + + public void setResultListType(ListTypes resultListType) { + this.resultListType = resultListType; + } + @Override public boolean equals(java.lang.Object o) { if (this == o) { diff --git a/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java b/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java index 4c8fd78d..a6d9fd54 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java @@ -221,8 +221,13 @@ public ResponseEntity searchListByItemRequest( validateAcceptHeader(request); Metadata metadata = generateMetaDataTemplate(body); - List> data = listItemService.search(listDbId, body, metadata); - return responseOK(new ListItemsListResponse(), new ListItemsListResponseResult(), data, metadata); + ListDetails list = listService.getList(listDbId); + List> data = listItemService.search(list, body, metadata); + + ListItemsListResponse listItemsListResponse = new ListItemsListResponse(); + listItemsListResponse.setResultListType(list.getListType()); + + return responseOK(listItemsListResponse, new ListItemsListResponseResult(), data, metadata); } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java index 6c46d4c0..316e60bc 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java @@ -34,11 +34,9 @@ public ListItemService(ListService listService, )); } - public List> search(String listDbId, + public List> search(ListDetails list, JsonNode searchRequest, Metadata metadata) throws BrAPIServerException { - ListDetails list = listService.getList(listDbId); - ListItemSearchRequestHandler handler = handlers.get(list.getListType()); if (handler == null) { From 500a83393a825ffce4c275f27e2a402d74f7d513 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Fri, 16 Oct 2026 15:21:04 -0400 Subject: [PATCH 47/56] Validate fields in submitted search request to ensure proper searches --- .../service/core/ListItemService.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java index 316e60bc..7e5df237 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java @@ -1,7 +1,10 @@ package org.brapi.test.BrAPITestServer.service.core; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; import io.swagger.model.Metadata; import io.swagger.model.core.*; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; @@ -11,8 +14,10 @@ import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.stereotype.Service; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -53,6 +58,8 @@ private List> executeHandler(ListItemSearchRequestHa T typedSearchRequest; + validateSearchRequest(list, request, handler); + try { typedSearchRequest = objectMapper.convertValue( request, @@ -69,4 +76,36 @@ private List> executeHandler(ListItemSearchRequestHa return handler.search(list, typedSearchRequest, metadata); } + + private void validateSearchRequest(ListDetails list, JsonNode request, ListItemSearchRequestHandler handler) throws BrAPIServerException { + JavaType javaType = objectMapper.getTypeFactory() + .constructType(handler.getSearchRequestClass()); + + BeanDescription beanDescription = objectMapper.getDeserializationConfig() + .introspect(javaType); + + Set allowedProperties = beanDescription.findProperties() + .stream() + .map(BeanPropertyDefinition::getName) + .collect(Collectors.toSet()); + + Iterator fields = request.fieldNames(); + + while (fields.hasNext()) { + String field = fields.next(); + + if (!allowedProperties.contains(field)) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, + String.format("List with dbId [%s] is of type [%s]. The expected search request to be submitted to this endpoint is a " + + "[%s], but a search request property [%s] was found that does not exist in the allowed search request for this list type. " + + "Please ensure any fields being searched match the schema of a [%s] exactly before submitting to ensure correct data is retrieved.", + list.getListDbId(), + list.getListType(), + handler.getSearchRequestClass().getSuperclass().getSimpleName(), + field, + handler.getSearchRequestClass().getSuperclass().getSimpleName()) + ); + } + } + } } From 6bb6f83bf0200885e2eedcdc6b7e781c9564e9d4 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 19 Oct 2026 13:54:29 -0400 Subject: [PATCH 48/56] Simplify validation logic --- .../service/core/ListItemService.java | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java index 7e5df237..59cfe885 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java @@ -1,9 +1,8 @@ package org.brapi.test.BrAPITestServer.service.core; -import com.fasterxml.jackson.databind.BeanDescription; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; import io.swagger.model.Metadata; import io.swagger.model.core.*; @@ -14,6 +13,7 @@ import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.stereotype.Service; +import java.io.IOException; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -78,23 +78,15 @@ private List> executeHandler(ListItemSearchRequestHa } private void validateSearchRequest(ListDetails list, JsonNode request, ListItemSearchRequestHandler handler) throws BrAPIServerException { - JavaType javaType = objectMapper.getTypeFactory() - .constructType(handler.getSearchRequestClass()); - BeanDescription beanDescription = objectMapper.getDeserializationConfig() - .introspect(javaType); + ObjectReader reader = objectMapper + .readerFor(handler.getSearchRequestClass()) + .with(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - Set allowedProperties = beanDescription.findProperties() - .stream() - .map(BeanPropertyDefinition::getName) - .collect(Collectors.toSet()); - Iterator fields = request.fieldNames(); - - while (fields.hasNext()) { - String field = fields.next(); - - if (!allowedProperties.contains(field)) { + try (JsonParser parser = request.traverse(objectMapper)) { + reader.readValue(parser); + } catch (UnrecognizedPropertyException e) { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, String.format("List with dbId [%s] is of type [%s]. The expected search request to be submitted to this endpoint is a " + "[%s], but a search request property [%s] was found that does not exist in the allowed search request for this list type. " + @@ -102,10 +94,19 @@ private void validateSearchRequest(ListDetails list, JsonNode request, ListI list.getListDbId(), list.getListType(), handler.getSearchRequestClass().getSuperclass().getSimpleName(), - field, + e.getPropertyName(), handler.getSearchRequestClass().getSuperclass().getSimpleName()) ); - } + } catch (IOException e) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, + String.format("List with dbId [%s] is of type [%s]. The expected search request to be submitted to this endpoint is a " + + "[%s], but a part of the request is malformed so that it is incompatible with the targeted request type." + + "Please ensure any fields being searched match the schema of a [%s] exactly before submitting to ensure correct data is retrieved.", + list.getListDbId(), + list.getListType(), + handler.getSearchRequestClass().getSuperclass().getSimpleName(), + handler.getSearchRequestClass().getSuperclass().getSimpleName()) + ); } } } From 4a31b8e8d8e4147a4f4947f70dc36f484bf43922 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 19 Oct 2026 15:35:07 -0400 Subject: [PATCH 49/56] Add clarifying comments --- .../model/core/GermplasmListSearchRequest.java | 2 +- .../swagger/model/core/ListItemWithFullItem.java | 1 + .../swagger/model/core/ListItemsListResponse.java | 1 + .../model/germ/GermplasmSearchRequest.java | 2 +- .../controller/core/BrAPIController.java | 1 + .../controller/core/ListsApiController.java | 1 + .../handlers/ListItemSearchRequestHandler.java | 10 ++++++++++ .../primaryEntities/BrAPIRepositoryImpl.java | 15 +++++++++++++++ .../service/core/ListItemService.java | 10 ++++++---- .../service/germ/GermplasmService.java | 15 ++++++++++++++- 10 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java b/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java index 2ece7db8..5768d82b 100644 --- a/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java +++ b/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java @@ -10,7 +10,7 @@ public class GermplasmListSearchRequest extends GermplasmSearchRequest { // The only real difference in this request is that there's an extra sort/filter field that GermplasmSearchRequest doesn't have. - // It is specific to lists. + // It is specific to lists. In general, other EntityListSearchRequests like this one should look the same, but have different base search requests. private static final Map LIST_ALLOWED_FIELDS; diff --git a/src/main/java/io/swagger/model/core/ListItemWithFullItem.java b/src/main/java/io/swagger/model/core/ListItemWithFullItem.java index f091c0a1..abe035c0 100644 --- a/src/main/java/io/swagger/model/core/ListItemWithFullItem.java +++ b/src/main/java/io/swagger/model/core/ListItemWithFullItem.java @@ -14,6 +14,7 @@ public class ListItemWithFullItem { @JsonProperty("listItem") private ListItem listItem = null; + // The full actual BrAPI model object of the list item is supplied/written into here @JsonProperty("item") private T item = null; diff --git a/src/main/java/io/swagger/model/core/ListItemsListResponse.java b/src/main/java/io/swagger/model/core/ListItemsListResponse.java index eb9c80d0..7b70e536 100644 --- a/src/main/java/io/swagger/model/core/ListItemsListResponse.java +++ b/src/main/java/io/swagger/model/core/ListItemsListResponse.java @@ -22,6 +22,7 @@ public class ListItemsListResponse implements BrAPIResponse searchListByItemRequest( ListDetails list = listService.getList(listDbId); List> data = listItemService.search(list, body, metadata); + // Setting the resultListType here allows the BrAPI client to know how to initialize the parameterized ListItemWithFullItem.item class ListItemsListResponse listItemsListResponse = new ListItemsListResponse(); listItemsListResponse.setResultListType(list.getListType()); diff --git a/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java b/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java index 12e988ed..29e3481f 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java @@ -8,6 +8,16 @@ import java.util.List; +// TODO: Implement for every available list type. For now, Germplasm is the only implementation required by DeltaBreed. +/** + * This is a generic interface for any list item search for any list type to implement. + * + * This interface is designed to have implementors pass a EntityListSearchRequest, like GermplasmListSearchRequest, as the parameterized object. + * The implementations must then provide the intended ListTypes enum value for this request, the EntityListSearchRequest class object, + * and a dedicated search method to carry out the search. + * + * See ListsApiController.searchListByItemRequest(), ListItemService.search() for usages, and GermplasmListSearchRequestHandler for an implementation. + */ public interface ListItemSearchRequestHandler { ListTypes getListType(); diff --git a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java index 61a01633..b0977e7c 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java @@ -60,6 +60,21 @@ public Page findAllBySearchAndPaginate(SearchQueryBuilder searchQuery, Pag return new PageImpl<>(content, pageReq, totalCount); } + /** + * Supports paginated queries that select on multiple entities, like this: + * + * SELECT listItem, entity FROM GermplasmEntity entity + * JOIN ListItemEntity listItem ON listItem.item = entity.germplasmName + * + * The resulting Page represents each entity as an element in the tuple from the select, so in the example above, + * tuple.get(0) = ListItemEntity + * tuple.get(1) = GermplasmEntity + * + * This is helpful when there are requirements to sort/filter/paginate an entity in relation to another object, which is the exact + * use case for ListsApiController.searchListByItemRequest(). + * + * For example usages of this method, see GermplasmService.findGermplasmByList() + */ public Page findAllBySearchAndPaginateMultipleEntitySelect(SearchQueryBuilder searchQuery, Pageable pageReq) throws InvalidPagingException { applyUserId(searchQuery); diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java index 59cfe885..1c846e07 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java @@ -3,21 +3,17 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; -import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; import io.swagger.model.Metadata; import io.swagger.model.core.*; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; import org.brapi.test.BrAPITestServer.handlers.ListItemSearchRequestHandler; -import org.brapi.test.BrAPITestServer.model.entity.core.ListItemEntity; import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.stereotype.Service; import java.io.IOException; -import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -32,6 +28,8 @@ public ListItemService(ListService listService, ObjectMapper objectMapper) { this.listService = listService; this.objectMapper = objectMapper; + // Initializes all ListItemSearchRequestHandlers by enumerating through the available BrAPI ListTypes. + // Right now, GermplasmListSearchRequestHandler is the only implementation available. this.handlers = handlerList.stream() .collect(Collectors.toMap( ListItemSearchRequestHandler::getListType, @@ -45,6 +43,7 @@ public List> search(ListDetails list, ListItemSearchRequestHandler handler = handlers.get(list.getListType()); if (handler == null) { + // List Type does not have a handler yet. Implement ListItemSearchRequestHandler for the BrAPI ListType, using GermplasmListSearchRequestHandler as an example. throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "Unsupported list type: " + list.getListType()); } @@ -77,6 +76,9 @@ private List> executeHandler(ListItemSearchRequestHa return handler.search(list, typedSearchRequest, metadata); } + /** + * Ensures submitted request contains fields/properties only relevant to the BrAPI search request for the list type. + */ private void validateSearchRequest(ListDetails list, JsonNode request, ListItemSearchRequestHandler handler) throws BrAPIServerException { ObjectReader reader = objectMapper 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 0f786abb..a6d0c03c 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 @@ -491,6 +491,9 @@ private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder> findGermplasmByList(ListDetails list, GermplasmListSearchRequest request, Metadata metadata) throws BrAPIServerException{ Page page = searchGermplasmByList(list, request, metadata); @@ -1185,10 +1188,11 @@ public Page searchGermplasmByList(ListDetails list, GermplasmListSearchRe if (sortByElements == null || sortByElements.isEmpty()) { // If there is no specified sort, sort by the position of the list. - SortBy defaultListItemSort = new SortBy("importEntryNumber", SortOrder.ASC); + SortBy defaultListItemSort = new SortBy("listItemPosition", SortOrder.ASC); searchRequest.setSortBy(List.of(defaultListItemSort)); } + // This generates the search query which has any filters, sorting, and where's derived from the incoming search request. SearchQueryBuilder searchQuery = buildGermplasmSearchQuery(searchRequest); searchQuery.modifySelect(List.of("listItem", "entity")); @@ -1198,6 +1202,15 @@ public Page searchGermplasmByList(ListDetails list, GermplasmListSearchRe .appendSingle(UUID.fromString(list.getListDbId()), "*list.id") .appendEnum(list.getListType(), "*list.listType"); + // At this point the search request should look something like: + // SELECT listItem, entity FROM GermplasmEntity entity + // JOIN ListItemEntity listItem ON entity.germplasmName = listItem.item + // JOIN listItem.list list + // (any other joins appearing came from buildGermplasmSearchQuery criteria) + // WHERE list.id IN (?) AND list.listType = ? AND (any other filters appearing came from buildGermplasmSearchQuery criteria) + // SORT BY (whatever is defined for listItemPosition in GermplasmListSearchRequest) ASC + // (If any other sorts appear here they were provided in the search request and were generated by buildGermplasmSearchQuery) + Page listItemAndGerm = germplasmRepository.findAllBySearchAndPaginateMultipleEntitySelect(searchQuery, pageReq); Page germs = listItemAndGerm.map( From 677d759c377f13acb2d85901426be0950a2426a6 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 19 Oct 2026 16:56:53 -0400 Subject: [PATCH 50/56] Fix alias bug with synonyms --- .../test/BrAPITestServer/service/SearchQueryBuilder.java | 2 +- .../BrAPITestServer/service/germ/GermplasmService.java | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) 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 bcd411ec..dbbdf04a 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -536,7 +536,7 @@ public SearchQueryBuilder filterBy(List filterBy, Map findGermplasmEntities(@Valid GermplasmSearchRequest // Since the built searchQuery contains a lazily loaded collection retrieved with a LEFT JOIN FETCH, // use findAllBySearchPaginatingWithFetches() + // TODO: As of BI-3082, the lazily loaded collection synonyms has been removed the initial search query. + // TODO: Because of this, we should be able to change this call to findAllBySearchAndPaginate, but this should be tracked as a separate effort. [BI-3084] Page germs = germplasmRepository.findAllBySearchPaginatingWithFetches(searchQuery, pageReq); // Hopefully this retains insertion order in the page? @@ -196,7 +198,7 @@ private SearchQueryBuilder buildGermplasmSearchQuery(GermplasmS .appendList(request.getStudyNames(), "*obsunit.study.studyName"); } if (request.getSynonyms() != null) { - searchQuery = searchQuery.join("synonyms", "synonym").appendList(request.getSynonyms(), "*synonym.synonym"); + searchQuery = searchQuery.join("synonyms", "synonyms").appendList(request.getSynonyms(), "*synonyms.synonym"); } if (request.getInstituteCodes() != null) { searchQuery = searchQuery.join("institutes", "institute").appendList(request.getSynonyms(), @@ -368,7 +370,7 @@ private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder synonyms = germplasmRepository.findAllBySearch(searchQuery); @@ -383,6 +385,7 @@ private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder xrefs = germplasmRepository.findAllBySearch(searchQuery); From aa58f0610c480b9721f2e2299ca203c2e05a89c9 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 19 Oct 2026 17:00:09 -0400 Subject: [PATCH 51/56] Add comment to BrAPIRepositoryImpl --- .../brapi/test/BrAPITestServer/service/SearchQueryBuilder.java | 2 ++ 1 file changed, 2 insertions(+) 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 dbbdf04a..62a50ad3 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -435,6 +435,8 @@ private String generateLeftJoinFetch(String join, String paramName) { return "LEFT JOIN FETCH " + entityPrefix(join) + " " + paramFilter(paramName) + " "; } + // Here * is used as a trigger to use the subsequent field string after it directly on a hibernate query instead of + // entity.field. This is important in many query builds, including filter/sort queries private String entityPrefix(String field) { if (field.startsWith("*")) { return field.substring(1); From 84ec9bd94e6b70cf475c9495c4c2afcb3d7c5bc6 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 19 Oct 2026 17:05:03 -0400 Subject: [PATCH 52/56] Add comments to SortAndFilterEntityColumnInfo --- .../model/dto/SortAndFilterEntityColumnInfo.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/brapi/test/BrAPITestServer/model/dto/SortAndFilterEntityColumnInfo.java b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/SortAndFilterEntityColumnInfo.java index 8fbf43e1..60cd42ed 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/model/dto/SortAndFilterEntityColumnInfo.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/SortAndFilterEntityColumnInfo.java @@ -7,7 +7,12 @@ public class SortAndFilterEntityColumnInfo { private final String entityColumnName; private final EntityType entityType; + // This boolean tells us effectively if the column is sortable. Hibernate does not support sorting on collections, as it is confusing. + // If this feature is desired, a new subquery would likely have to be created to achieve this on the collection. + // See SearchQueryBuilder.sortBy() for usage. private final boolean isCollectionOnEntity; + // This boolean tells us if the column has been joined on a table that does not exist in the target entity. + // In this case it's likely we want different join logic, see SearchQueryBuilder.sortBy or .filterBy for usages private final boolean isJoinOnUnrelatedEntity; From 1fc0445c80607f5b8b55eb26e6f5a69f475468d0 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 19 Oct 2026 17:08:13 -0400 Subject: [PATCH 53/56] Add comment for extra synonyms join --- .../test/BrAPITestServer/service/germ/GermplasmService.java | 2 ++ 1 file changed, 2 insertions(+) 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 25019e79..a6c6dbbf 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 @@ -385,6 +385,8 @@ private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder xrefs = germplasmRepository.findAllBySearch(searchQuery); From f7593b24b3a76f293ab39606ed2a2c107ca6985c Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Thu, 22 Oct 2026 15:39:47 -0400 Subject: [PATCH 54/56] Fix synonym wrong object setting/getting, SearchRequest sort/filter * columns --- .../java/io/swagger/model/core/StudySearchRequest.java | 10 +++++----- .../java/io/swagger/model/core/TrialSearchRequest.java | 2 +- .../BrAPITestServer/service/germ/GermplasmService.java | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/io/swagger/model/core/StudySearchRequest.java b/src/main/java/io/swagger/model/core/StudySearchRequest.java index 26d5f223..a9c3c9cd 100644 --- a/src/main/java/io/swagger/model/core/StudySearchRequest.java +++ b/src/main/java/io/swagger/model/core/StudySearchRequest.java @@ -19,14 +19,14 @@ public class StudySearchRequest extends SearchRequest { private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = Map.ofEntries( Map.entry("commonCropName", new SortAndFilterEntityColumnInfo("crop.cropName", EntityType.TEXT)), - Map.entry("externalReferenceId", new SortAndFilterEntityColumnInfo("*externalReferences.externalReferenceId", EntityType.TEXT)), - Map.entry("externalReferenceSource", new SortAndFilterEntityColumnInfo("*externalReferences.externalReferenceSource", EntityType.TEXT)), - Map.entry("germplasmDbId", new SortAndFilterEntityColumnInfo("*obsunit.germplasm.id", EntityType.UUID)), + Map.entry("externalReferenceId", new SortAndFilterEntityColumnInfo("*externalReferences.externalReferenceId", EntityType.TEXT, true, false)), + Map.entry("externalReferenceSource", new SortAndFilterEntityColumnInfo("*externalReferences.externalReferenceSource", EntityType.TEXT, true, false)), + Map.entry("germplasmDbId", new SortAndFilterEntityColumnInfo("*obsunit.germplasm.id", EntityType.UUID, true, false)), Map.entry("locationDbId", new SortAndFilterEntityColumnInfo("location.id", EntityType.UUID)), - Map.entry("observationVariableDbId", new SortAndFilterEntityColumnInfo("*observation.observationVariable.id", EntityType.UUID)), + Map.entry("observationVariableDbId", new SortAndFilterEntityColumnInfo("*observation.observationVariable.id", EntityType.UUID, true, false)), Map.entry("programDbId", new SortAndFilterEntityColumnInfo("trial.program.id", EntityType.UUID)), Map.entry("programName", new SortAndFilterEntityColumnInfo("trial.program.name", EntityType.TEXT)), - Map.entry("seasonDbId", new SortAndFilterEntityColumnInfo("*season.id", EntityType.UUID)), + Map.entry("seasonDbId", new SortAndFilterEntityColumnInfo("*season.id", EntityType.UUID, true, false)), Map.entry("studyCode", new SortAndFilterEntityColumnInfo("studyCode", EntityType.TEXT)), Map.entry("studyDbId", new SortAndFilterEntityColumnInfo("id", EntityType.UUID)), Map.entry("studyLocation", new SortAndFilterEntityColumnInfo("location.id", EntityType.UUID)), diff --git a/src/main/java/io/swagger/model/core/TrialSearchRequest.java b/src/main/java/io/swagger/model/core/TrialSearchRequest.java index c4f1c293..f703010e 100644 --- a/src/main/java/io/swagger/model/core/TrialSearchRequest.java +++ b/src/main/java/io/swagger/model/core/TrialSearchRequest.java @@ -24,7 +24,7 @@ public class TrialSearchRequest extends SearchRequest { "endDate", new SortAndFilterEntityColumnInfo("endDate", EntityType.TEXT), "active", new SortAndFilterEntityColumnInfo("active", EntityType.BOOLEAN), "programName", new SortAndFilterEntityColumnInfo("program.name", EntityType.TEXT), - "locationDbId", new SortAndFilterEntityColumnInfo("*studies.location.id", EntityType.UUID) + "locationDbId", new SortAndFilterEntityColumnInfo("*studies.location.id", EntityType.UUID, true, false) ); @JsonProperty("commonCropNames") 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 a6c6dbbf..2ffa2ac4 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 @@ -374,10 +374,10 @@ private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder synonyms = germplasmRepository.findAllBySearch(searchQuery); - Map> synonymsByEntity = new HashMap<>(); - synonyms.forEach(entity -> synonymsByEntity.put(entity.getId().toString(), entity.getExternalReferences())); + Map> synonymsByEntity = new HashMap<>(); + synonyms.forEach(entity -> synonymsByEntity.put(entity.getId().toString(), entity.getSynonyms())); - germEntities.forEach(entity -> entity.setExternalReferences(synonymsByEntity.get(entity.getId().toString()))); + germEntities.forEach(entity -> entity.setSynonyms(synonymsByEntity.get(entity.getId().toString()))); // Fetch xrefs log.debug("Fetching xrefs"); From 6e8e32dee24f250ec58a038a1eba334ed9fccd87 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Thu, 22 Oct 2026 16:24:28 -0400 Subject: [PATCH 55/56] Only re-add synonym join if filter present --- .../service/germ/GermplasmService.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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 2ffa2ac4..13c45b7e 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 @@ -175,7 +175,7 @@ public List findGermplasmEntitiesWithoutPaging(@Valid Germplasm List germs = germplasmRepository.findAllBySearch(searchQuery); if (!germs.isEmpty()) { - fetchRemainingGermCollectionsUsingQuery(searchQuery, germs); + fetchRemainingGermCollectionsUsingQuery(searchQuery, germs, request); } return germs; } @@ -201,7 +201,7 @@ private SearchQueryBuilder buildGermplasmSearchQuery(GermplasmS searchQuery = searchQuery.join("synonyms", "synonyms").appendList(request.getSynonyms(), "*synonyms.synonym"); } if (request.getInstituteCodes() != null) { - searchQuery = searchQuery.join("institutes", "institute").appendList(request.getSynonyms(), + searchQuery = searchQuery.join("institutes", "institute").appendList(request.getInstituteCodes(), "*institute.instituteCode"); } @@ -359,7 +359,7 @@ private void fetchPedigreeEdges(List ids, Page germEntiti }); } - private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder searchQuery, List germEntities) { + private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder searchQuery, List germEntities, GermplasmSearchRequest request) { // Remove unused left join fetches from the base query searchQuery.removeLeftJoinFetch("breedingMethod", "breedingMethod") .removeLeftJoinFetch("crop", "crop") @@ -385,9 +385,11 @@ private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder xrefs = germplasmRepository.findAllBySearch(searchQuery); From 4db1b4fa3e55aecc7c173c21a5f8952c41e2a943 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Fri, 23 Oct 2026 16:42:11 -0400 Subject: [PATCH 56/56] Remove 202 from searchListByItemRequest, not relevant --- src/main/java/io/swagger/api/core/ListsApi.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/io/swagger/api/core/ListsApi.java b/src/main/java/io/swagger/api/core/ListsApi.java index 8d346d29..398c7cb5 100644 --- a/src/main/java/io/swagger/api/core/ListsApi.java +++ b/src/main/java/io/swagger/api/core/ListsApi.java @@ -166,7 +166,6 @@ ResponseEntity searchListsSearchResultsDbIdGet( @ApiOperation(value = "Submit a search request for List Items", nickname = "searchListByItemRequest", notes = "Advanced searching for the list item resource. See Search Services for additional implementation details.", response = ListItemsListResponse.class, authorizations = { @Authorization(value = "AuthorizationToken") }, tags = { "Lists", }) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = ListItemsListResponse.class), - @ApiResponse(code = 202, message = "Accepted", response = Model202AcceptedSearchResponse.class), @ApiResponse(code = 400, message = "Bad Request", response = String.class), @ApiResponse(code = 401, message = "Unauthorized", response = String.class), @ApiResponse(code = 403, message = "Forbidden", response = String.class) })