From 3640e5d6b221ea64c296f1deb581d6b247718fbf Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Thu, 7 May 2026 15:01:45 -0400 Subject: [PATCH 01/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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 1110225e3b910068d608f7d69cbcb10110b9861d Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 1 Jun 2026 15:48:51 -0400 Subject: [PATCH 08/16] 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 3bf581ada9b97adcff06bd7346a40391f5ce01a4 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 1 Jun 2026 15:48:51 -0400 Subject: [PATCH 09/16] 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 5e05e31ba8c6801fbfdf7f5695d9f39dd2c983c8 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 2 Jun 2026 08:57:52 -0400 Subject: [PATCH 10/16] Changes to pom to support deploy failures --- 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 e079626f178592c6c270a8160dde5f23dfd1ec68 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Fri, 24 Jul 2026 12:54:11 -0400 Subject: [PATCH 11/16] 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 12/16] 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 13/16] 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 14/16] 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 15/16] 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 16/16] 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.