From 091f833e37bf9778fed873d1b6a8599351de0b04 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 12 Oct 2026 19:56:03 -0400 Subject: [PATCH 01/15] Lay groundwork for generic ListItem implementation --- .../java/io/swagger/api/core/ListsApi.java | 18 ++- .../core/GermplasmListSearchRequest.java | 35 +++++ .../java/io/swagger/model/core/ListItem.java | 100 ++++++++++++++ .../model/core/ListItemSearchRequest.java | 21 +++ .../model/core/ListItemWithFullItem.java | 82 ++++++++++++ .../model/core/ListItemsListResponse.java | 122 ++++++++++++++++++ .../core/ListItemsListResponseResult.java | 87 +++++++++++++ .../model/germ/GermplasmSearchRequest.java | 9 +- .../controller/core/ListsApiController.java | 21 ++- .../GermplasmListSearchRequestHandler.java | 36 ++++++ .../ListItemSearchRequestHandler.java | 20 +++ .../service/core/ListItemService.java | 59 +++++++++ .../service/germ/GermplasmService.java | 7 + 13 files changed, 607 insertions(+), 10 deletions(-) create mode 100644 src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java create mode 100644 src/main/java/io/swagger/model/core/ListItem.java create mode 100644 src/main/java/io/swagger/model/core/ListItemSearchRequest.java create mode 100644 src/main/java/io/swagger/model/core/ListItemWithFullItem.java create mode 100644 src/main/java/io/swagger/model/core/ListItemsListResponse.java create mode 100644 src/main/java/io/swagger/model/core/ListItemsListResponseResult.java create mode 100644 src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java create mode 100644 src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java create mode 100644 src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java diff --git a/src/main/java/io/swagger/api/core/ListsApi.java b/src/main/java/io/swagger/api/core/ListsApi.java index b4c95e98..71acc643 100644 --- a/src/main/java/io/swagger/api/core/ListsApi.java +++ b/src/main/java/io/swagger/api/core/ListsApi.java @@ -7,11 +7,7 @@ import io.swagger.model.BrAPIResponse; import io.swagger.model.Model202AcceptedSearchResponse; -import io.swagger.model.core.ListNewRequest; -import io.swagger.model.core.ListResponse; -import io.swagger.model.core.ListSearchRequest; -import io.swagger.model.core.ListsListResponse; -import io.swagger.model.core.ListsSingleResponse; +import io.swagger.model.core.*; import io.swagger.annotations.*; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; @@ -166,4 +162,16 @@ ResponseEntity searchListsSearchResultsDbIdGet( @ApiParam(value = "pageSize") @Valid @RequestParam(value = "pageSize", required = false) Integer pageSize, @ApiParam(value = "HTTP HEADER - Token used for Authorization Bearer {token_string} ") @RequestHeader(value = "Authorization", required = false) String authorization) throws BrAPIServerException; + @ApiOperation(value = "Submit a search request for List Items", nickname = "searchListByItemRequest", notes = "Advanced searching for the list item resource. See Search Services for additional implementation details.", response = ListItemsListResponse.class, authorizations = { + @Authorization(value = "AuthorizationToken") }, tags = { "Lists", }) + @ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = ListItemsListResponse.class), + @ApiResponse(code = 202, message = "Accepted", response = Model202AcceptedSearchResponse.class), + @ApiResponse(code = 400, message = "Bad Request", response = String.class), + @ApiResponse(code = 401, message = "Unauthorized", response = String.class), + @ApiResponse(code = 403, message = "Forbidden", response = String.class) }) + @RequestMapping(value = "/search/lists/{listDbId}/items", produces = { "application/json" }, consumes = { + "application/json" }, method = RequestMethod.POST) + ResponseEntity searchListByItemRequest(@ApiParam(value = "") @Valid @RequestBody ListItemSearchRequest body, + @ApiParam(value = "Permanent unique identifier of list to perform search on", required = true) @PathVariable("listDbId") String listDbId, + @ApiParam(value = "HTTP HEADER - Token used for Authorization Bearer {token_string} ") @RequestHeader(value = "Authorization", required = false) String authorization) throws BrAPIServerException; } diff --git a/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java b/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java new file mode 100644 index 00000000..2d50ac7c --- /dev/null +++ b/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java @@ -0,0 +1,35 @@ +package io.swagger.model.core; + +import io.swagger.model.germ.GermplasmSearchRequest; +import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; +import org.brapi.test.BrAPITestServer.model.dto.EntityType; + +import java.util.Collections; +import java.util.Map; + +public class GermplasmListSearchRequest extends GermplasmSearchRequest { + + // The only real difference in this request is that there's an extra sort/filter field that GermplasmSearchRequest doesn't have. + // It is specific to lists. + private static final Map + LIST_ALLOWED_FIELDS; + + static { + Map fields = + copyAllowedSortAndFilterFields(); + + fields.put( + "listItemPosition", + new EntityColumnNameAndType("listItem.position", EntityType.NUMBER) + ); + + LIST_ALLOWED_FIELDS = Collections.unmodifiableMap(fields); + } + + @Override + public Map getEntityColAndTypeBySubmittedNameMap() { + return LIST_ALLOWED_FIELDS; + } + + +} diff --git a/src/main/java/io/swagger/model/core/ListItem.java b/src/main/java/io/swagger/model/core/ListItem.java new file mode 100644 index 00000000..0627eb7b --- /dev/null +++ b/src/main/java/io/swagger/model/core/ListItem.java @@ -0,0 +1,100 @@ +package io.swagger.model.core; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import org.springframework.validation.annotation.Validated; + +/** + * ListItem + */ +@Validated +@javax.annotation.processing.Generated(value = "io.swagger.codegen.v3.generators.java.SpringCodegen", date = "2020-03-20T16:31:52.030Z[GMT]") +public class ListItem { + @JsonProperty("listItemDbId") + private String listItemDbId = null; + + @JsonProperty("position") + private Integer position = null; + + // This represents the actual value stored in the list_item table + @JsonProperty("item") + private String item = null; + + public ListItem listDbId(String listDbId) { + this.listItemDbId = listDbId; + return this; + } + + /** + * The unique identifier for a List + * @return listDbId + **/ + @ApiModelProperty(example = "6f621cfa", required = true, value = "The unique identifier for a List") + + + public String getListItemDbId() { + return listItemDbId; + } + + public void setListItemDbId(String listItemDbId) { + this.listItemDbId = listItemDbId; + } + + public Integer getPosition() { + return position; + } + + public void setPosition(Integer position) { + this.position = position; + } + + public String getItem() { + return item; + } + + public void setItem(String item) { + this.item = item; + } + + @Override + public boolean equals(java.lang.Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ListItem ListItem = (ListItem) o; + return Objects.equals(this.listItemDbId, ListItem.listItemDbId) && + super.equals(o); + } + + @Override + public int hashCode() { + return Objects.hash(listItemDbId, super.hashCode()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ListItem {\n"); + sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + sb.append(" listDbId: ").append(toIndentedString(listItemDbId)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + + diff --git a/src/main/java/io/swagger/model/core/ListItemSearchRequest.java b/src/main/java/io/swagger/model/core/ListItemSearchRequest.java new file mode 100644 index 00000000..dc9e4949 --- /dev/null +++ b/src/main/java/io/swagger/model/core/ListItemSearchRequest.java @@ -0,0 +1,21 @@ +package io.swagger.model.core; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.model.SearchRequest; + +public class ListItemSearchRequest { + + @JsonProperty("searchRequest") + private JsonNode searchRequest; + + // List-item-specific filters and sorting can go here. + + public JsonNode getSearchRequest() { + return searchRequest; + } + + public void setSearchRequest(JsonNode searchRequest) { + this.searchRequest = searchRequest; + } +} diff --git a/src/main/java/io/swagger/model/core/ListItemWithFullItem.java b/src/main/java/io/swagger/model/core/ListItemWithFullItem.java new file mode 100644 index 00000000..bacfb5bf --- /dev/null +++ b/src/main/java/io/swagger/model/core/ListItemWithFullItem.java @@ -0,0 +1,82 @@ +package io.swagger.model.core; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import org.springframework.validation.annotation.Validated; + +/** + * ListItemWithFullItem + */ +@Validated +@javax.annotation.processing.Generated(value = "io.swagger.codegen.v3.generators.java.SpringCodegen", date = "2020-03-20T16:31:52.030Z[GMT]") +public class ListItemWithFullItem { + @JsonProperty("listItem") + private ListItem listItem = null; + + @JsonProperty("item") + private T item = null; + + /** + * The unique identifier for a List + * @return listItem + **/ + @ApiModelProperty(example = "6f621cfa", required = true, value = "The unique identifier for a List") + + + public ListItem getlistItem() { + return listItem; + } + + public void setlistItem(ListItem listItem) { + this.listItem = listItem; + } + + public T getItem() { + return item; + } + + public void setItem(T item) { + this.item = item; + } + + @Override + public boolean equals(java.lang.Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ListItemWithFullItem listItemWithFullItem = (ListItemWithFullItem) o; + return Objects.equals(this.listItem, listItemWithFullItem.listItem) && + super.equals(o); + } + + @Override + public int hashCode() { + return Objects.hash(listItem, super.hashCode()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ListItemWithFullItem {\n"); + sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + sb.append(" listItem: ").append(toIndentedString(listItem)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/src/main/java/io/swagger/model/core/ListItemsListResponse.java b/src/main/java/io/swagger/model/core/ListItemsListResponse.java new file mode 100644 index 00000000..e36f43e7 --- /dev/null +++ b/src/main/java/io/swagger/model/core/ListItemsListResponse.java @@ -0,0 +1,122 @@ +package io.swagger.model.core; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import io.swagger.model.BrAPIResponse; +import io.swagger.model.Context; +import io.swagger.model.Metadata; + +import org.springframework.validation.annotation.Validated; +import jakarta.validation.Valid; + +/** + * ListItemsListResponse + */ +@Validated +@javax.annotation.processing.Generated(value = "io.swagger.codegen.v3.generators.java.SpringCodegen", date = "2020-03-20T16:31:52.030Z[GMT]") +public class ListItemsListResponse implements BrAPIResponse { + @JsonProperty("@context") + private Context _atContext = null; + + @JsonProperty("metadata") + private Metadata metadata = null; + + @JsonProperty("result") + private ListItemsListResponseResult result = null; + + public ListItemsListResponse() { + this._atContext = new Context(); + this._atContext.add("context"); + } + + + public void set_atContext(Context _atContext) { + this._atContext = _atContext; + } + public ListItemsListResponse metadata(Metadata metadata) { + this.metadata = metadata; + return this; + } + + /** + * Get metadata + * + * @return metadata + **/ + @ApiModelProperty(required = true, value = "") + + + @Valid + public Metadata getMetadata() { + return metadata; + } + + public void setMetadata(Metadata metadata) { + this.metadata = metadata; + } + + public ListItemsListResponse result(ListItemsListResponseResult result) { + this.result = result; + return this; + } + + /** + * Get result + * + * @return result + **/ + @ApiModelProperty(required = true, value = "") + + + @Valid + public ListItemsListResponseResult getResult() { + return result; + } + + public void setResult(ListItemsListResponseResult result) { + this.result = result; + } + + @Override + public boolean equals(java.lang.Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ListItemsListResponse listItemsListResponse = (ListItemsListResponse) o; + return Objects.equals(this._atContext, listItemsListResponse._atContext) + && Objects.equals(this.metadata, listItemsListResponse.metadata) + && Objects.equals(this.result, listItemsListResponse.result); + } + + @Override + public int hashCode() { + return Objects.hash(_atContext, metadata, result); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ListItemsListResponse {\n"); + + sb.append(" _atContext: ").append(toIndentedString(_atContext)).append("\n"); + sb.append(" metadata: ").append(toIndentedString(metadata)).append("\n"); + sb.append(" result: ").append(toIndentedString(result)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/src/main/java/io/swagger/model/core/ListItemsListResponseResult.java b/src/main/java/io/swagger/model/core/ListItemsListResponseResult.java new file mode 100644 index 00000000..6a5f6561 --- /dev/null +++ b/src/main/java/io/swagger/model/core/ListItemsListResponseResult.java @@ -0,0 +1,87 @@ +package io.swagger.model.core; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import io.swagger.model.BrAPIResponseResult; + +import java.util.ArrayList; +import java.util.List; +import org.springframework.validation.annotation.Validated; +import jakarta.validation.Valid; + +/** + * ListItemsListResponseResult + */ +@Validated +@javax.annotation.processing.Generated(value = "io.swagger.codegen.v3.generators.java.SpringCodegen", date = "2020-03-20T16:31:52.030Z[GMT]") +public class ListItemsListResponseResult implements BrAPIResponseResult> { + @JsonProperty("data") + @Valid + private List> data = new ArrayList>(); + + public ListItemsListResponseResult data(List> data) { + this.data = data; + return this; + } + + public ListItemsListResponseResult addDataItem(ListItemWithFullItem dataItem) { + this.data.add(dataItem); + return this; + } + + /** + * Get data + * @return data + **/ + @ApiModelProperty(required = true, value = "") + + @Valid + public List> getData() { + return data; + } + + public void setData(List> data) { + this.data = data; + } + + + @Override + public boolean equals(java.lang.Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ListItemsListResponseResult listItemsListResponseResult = (ListItemsListResponseResult) o; + return Objects.equals(this.data, listItemsListResponseResult.data); + } + + @Override + public int hashCode() { + return Objects.hash(data); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ListItemsListResponseResult {\n"); + + sb.append(" data: ").append(toIndentedString(data)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java b/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java index ed030625..029ab807 100644 --- a/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java +++ b/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java @@ -6,10 +6,7 @@ import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; import org.brapi.test.BrAPITestServer.model.dto.EntityType; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; public class GermplasmSearchRequest extends SearchRequest { @@ -31,6 +28,10 @@ public class GermplasmSearchRequest extends SearchRequest { Map.entry("synonyms", new EntityColumnNameAndType("*synonyms.synonym", EntityType.TEXT)) ); + protected static Map copyAllowedSortAndFilterFields() { + return new HashMap<>(ALLOWED_SORT_AND_FILTER_FIELDS); + } + @JsonProperty("accessionNumbers") private List accessionNumbers = null; diff --git a/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java b/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java index ce653cdf..92af758a 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java @@ -12,6 +12,7 @@ import org.brapi.test.BrAPITestServer.model.entity.SearchRequestEntity; import org.brapi.test.BrAPITestServer.model.entity.SearchRequestEntity.SearchRequestTypes; import org.brapi.test.BrAPITestServer.service.SearchService; +import org.brapi.test.BrAPITestServer.service.core.ListItemService; import org.brapi.test.BrAPITestServer.service.core.ListService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,12 +40,14 @@ public class ListsApiController extends BrAPIController implements ListsApi { private final HttpServletRequest request; private final ListService listService; private final SearchService searchService; + private final ListItemService listItemService; @Autowired - public ListsApiController(ListService listService, SearchService searchService, HttpServletRequest request) { + public ListsApiController(ListService listService, SearchService searchService, HttpServletRequest request, ListItemService listItemService) { this.listService = listService; this.searchService = searchService; this.request = request; + this.listItemService = listItemService; } @CrossOrigin @@ -205,4 +208,20 @@ public ResponseEntity searchListsSearchResultsDbIdGet( } } + @CrossOrigin + @Override + public ResponseEntity searchListByItemRequest( + @Valid @RequestBody ListItemSearchRequest body, + @PathVariable("listDbId") String listDbId, + @RequestHeader(value = "Authorization", required = false) String authorization) throws BrAPIServerException { + + log.debug("Request: " + request.getRequestURI()); + validateSecurityContext(request, "ROLE_ANONYMOUS", "ROLE_USER"); + validateAcceptHeader(request); + Metadata metadata = generateEmptyMetadata(); + + List> data = listItemService.search(listDbId, body); + return responseOK(new ListItemsListResponse(), new ListItemsListResponseResult(), data, metadata); + } + } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java b/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java new file mode 100644 index 00000000..9b7ffe94 --- /dev/null +++ b/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java @@ -0,0 +1,36 @@ +package org.brapi.test.BrAPITestServer.handlers; + +import io.swagger.model.core.GermplasmListSearchRequest; +import io.swagger.model.core.ListDetails; +import io.swagger.model.core.ListItemWithFullItem; +import io.swagger.model.core.ListTypes; +import org.brapi.test.BrAPITestServer.service.germ.GermplasmService; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class GermplasmListSearchRequestHandler implements ListItemSearchRequestHandler { + private final GermplasmService germplasmService; + + public GermplasmListSearchRequestHandler(GermplasmService germplasmService) { + this.germplasmService = germplasmService; + } + + @Override + public ListTypes getListType() { + return ListTypes.GERMPLASM; + } + + @Override + public Class getSearchRequestClass() { + return GermplasmListSearchRequest.class; + } + + @Override + public List> search( + ListDetails list, + GermplasmListSearchRequest searchRequest) { + return germplasmService.searchGermplasmByList(list, searchRequest); + } +} diff --git a/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java b/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java new file mode 100644 index 00000000..ef0fe896 --- /dev/null +++ b/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java @@ -0,0 +1,20 @@ +package org.brapi.test.BrAPITestServer.handlers; + +import io.swagger.model.core.ListDetails; +import io.swagger.model.core.ListItemWithFullItem; +import io.swagger.model.core.ListItemsListResponseResult; +import io.swagger.model.core.ListTypes; +import org.brapi.test.BrAPITestServer.model.entity.core.ListEntity; + +import java.util.List; + +public interface ListItemSearchRequestHandler { + ListTypes getListType(); + + Class getSearchRequestClass(); + + List> search( + ListDetails list, + T searchRequest + ); +} diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java new file mode 100644 index 00000000..d1f17c3c --- /dev/null +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java @@ -0,0 +1,59 @@ +package org.brapi.test.BrAPITestServer.service.core; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.swagger.model.core.*; +import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; +import org.brapi.test.BrAPITestServer.handlers.ListItemSearchRequestHandler; +import org.brapi.test.BrAPITestServer.model.entity.core.ListEntity; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +public class ListItemService { + private final ListService listService; + private final Map> handlers; + private final ObjectMapper objectMapper; + + public ListItemService(ListService listService, + List> handlerList, + ObjectMapper objectMapper) { + this.listService = listService; + this.objectMapper = objectMapper; + this.handlers = handlerList.stream() + .collect(Collectors.toMap( + ListItemSearchRequestHandler::getListType, + Function.identity() + )); + } + + public List> search(String listDbId, + ListItemSearchRequest searchRequest) throws BrAPIServerException { + ListDetails list = listService.getList(listDbId); + + ListItemSearchRequestHandler handler = handlers.get(list.getListType()); + + if (handler == null) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "Unsupported list type: " + list.getListType()); + } + + return executeHandler(handler, list, searchRequest); + } + + private List> executeHandler( + ListItemSearchRequestHandler handler, + ListDetails list, + ListItemSearchRequest request) { + + T typedSearchRequest = objectMapper.convertValue( + request.getSearchRequest(), + handler.getSearchRequestClass() + ); + + return handler.search(list, typedSearchRequest); + } +} diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java index 36485c6d..c5ac8e80 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java @@ -4,6 +4,9 @@ import java.util.*; import java.util.stream.Collectors; +import io.swagger.model.core.GermplasmListSearchRequest; +import io.swagger.model.core.ListDetails; +import io.swagger.model.core.ListItemWithFullItem; import io.swagger.model.core.ProgramSearchRequest; import io.swagger.model.germ.*; import jakarta.validation.Valid; @@ -455,6 +458,10 @@ private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder> searchGermplasmByList(ListDetails list, GermplasmListSearchRequest request) { + return new ArrayList<>(); + } + public Germplasm getGermplasm(String germplasmDbId) throws BrAPIServerException { return convertFromEntity(getGermplasmEntity(germplasmDbId, HttpStatus.NOT_FOUND)); } From 5e18e83c45c8f36378ff9a7efafd15552a2c88d9 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 13 Oct 2026 13:28:47 -0400 Subject: [PATCH 02/15] Fix some bugs, add working example query --- .../java/io/swagger/api/core/ListsApi.java | 3 +- .../model/core/ListItemSearchRequest.java | 1 - .../controller/core/BrAPIController.java | 20 +++++++++ .../controller/core/ListsApiController.java | 7 ++-- .../GermplasmListSearchRequestHandler.java | 42 ++++++++++++++++--- .../ListItemSearchRequestHandler.java | 9 ++-- .../primaryEntities/BrAPIRepository.java | 4 +- .../primaryEntities/BrAPIRepositoryImpl.java | 13 ++++++ .../service/SearchQueryBuilder.java | 26 ++++++++++++ .../service/core/ListItemService.java | 38 +++++++++++------ .../service/germ/GermplasmService.java | 9 +++- 11 files changed, 143 insertions(+), 29 deletions(-) diff --git a/src/main/java/io/swagger/api/core/ListsApi.java b/src/main/java/io/swagger/api/core/ListsApi.java index 71acc643..8d346d29 100644 --- a/src/main/java/io/swagger/api/core/ListsApi.java +++ b/src/main/java/io/swagger/api/core/ListsApi.java @@ -5,6 +5,7 @@ */ package io.swagger.api.core; +import com.fasterxml.jackson.databind.JsonNode; import io.swagger.model.BrAPIResponse; import io.swagger.model.Model202AcceptedSearchResponse; import io.swagger.model.core.*; @@ -171,7 +172,7 @@ ResponseEntity searchListsSearchResultsDbIdGet( @ApiResponse(code = 403, message = "Forbidden", response = String.class) }) @RequestMapping(value = "/search/lists/{listDbId}/items", produces = { "application/json" }, consumes = { "application/json" }, method = RequestMethod.POST) - ResponseEntity searchListByItemRequest(@ApiParam(value = "") @Valid @RequestBody ListItemSearchRequest body, + ResponseEntity searchListByItemRequest(@ApiParam(value = "") @Valid @RequestBody JsonNode body, @ApiParam(value = "Permanent unique identifier of list to perform search on", required = true) @PathVariable("listDbId") String listDbId, @ApiParam(value = "HTTP HEADER - Token used for Authorization Bearer {token_string} ") @RequestHeader(value = "Authorization", required = false) String authorization) throws BrAPIServerException; } diff --git a/src/main/java/io/swagger/model/core/ListItemSearchRequest.java b/src/main/java/io/swagger/model/core/ListItemSearchRequest.java index dc9e4949..c5fe9b33 100644 --- a/src/main/java/io/swagger/model/core/ListItemSearchRequest.java +++ b/src/main/java/io/swagger/model/core/ListItemSearchRequest.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; -import io.swagger.model.SearchRequest; public class ListItemSearchRequest { diff --git a/src/main/java/org/brapi/test/BrAPITestServer/controller/core/BrAPIController.java b/src/main/java/org/brapi/test/BrAPITestServer/controller/core/BrAPIController.java index 05cd18bf..42099bcd 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/controller/core/BrAPIController.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/controller/core/BrAPIController.java @@ -5,6 +5,7 @@ import java.util.Set; import java.util.stream.Collectors; +import com.fasterxml.jackson.databind.JsonNode; import jakarta.servlet.http.HttpServletRequest; import io.swagger.model.core.BatchDeletesListResponse; @@ -49,6 +50,25 @@ protected Metadata generateMetaDataTemplate(String pageToken, Integer pageSize) return metaData; } + protected Metadata generateMetaDataTemplate(JsonNode node) throws BrAPIServerException { + + Integer page = null; + Integer pageSize = null; + + JsonNode pageAsNode = node.get("page"); + JsonNode pageSizeAsNode = node.get("pageSize"); + + if (pageAsNode != null && pageAsNode.canConvertToInt()) { + page = pageAsNode.asInt(); + } + + if (pageSizeAsNode != null && pageSizeAsNode.canConvertToInt()) { + pageSize = pageSizeAsNode.asInt(); + } + + return generateMetaDataTemplate(page, pageSize); + } + protected Metadata generateMetaDataTemplate(Integer page, Integer pageSize) throws BrAPIServerException { PagingUtility.validatePaging(page, pageSize); diff --git a/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java b/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java index 92af758a..4c8fd78d 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java @@ -1,5 +1,6 @@ package org.brapi.test.BrAPITestServer.controller.core; +import com.fasterxml.jackson.databind.JsonNode; import io.swagger.model.BrAPIResponse; import io.swagger.model.Metadata; import io.swagger.model.core.*; @@ -211,16 +212,16 @@ public ResponseEntity searchListsSearchResultsDbIdGet( @CrossOrigin @Override public ResponseEntity searchListByItemRequest( - @Valid @RequestBody ListItemSearchRequest body, + @Valid @RequestBody JsonNode body, @PathVariable("listDbId") String listDbId, @RequestHeader(value = "Authorization", required = false) String authorization) throws BrAPIServerException { log.debug("Request: " + request.getRequestURI()); validateSecurityContext(request, "ROLE_ANONYMOUS", "ROLE_USER"); validateAcceptHeader(request); - Metadata metadata = generateEmptyMetadata(); + Metadata metadata = generateMetaDataTemplate(body); - List> data = listItemService.search(listDbId, body); + List> data = listItemService.search(listDbId, body, metadata); return responseOK(new ListItemsListResponse(), new ListItemsListResponseResult(), data, metadata); } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java b/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java index 9b7ffe94..957d83e0 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java @@ -1,12 +1,16 @@ package org.brapi.test.BrAPITestServer.handlers; -import io.swagger.model.core.GermplasmListSearchRequest; -import io.swagger.model.core.ListDetails; -import io.swagger.model.core.ListItemWithFullItem; -import io.swagger.model.core.ListTypes; +import io.swagger.model.Metadata; +import io.swagger.model.core.*; +import io.swagger.model.germ.Germplasm; +import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; +import org.brapi.test.BrAPITestServer.model.entity.core.ListItemEntity; +import org.brapi.test.BrAPITestServer.model.entity.germ.GermplasmEntity; +import org.brapi.test.BrAPITestServer.service.PagingUtility; import org.brapi.test.BrAPITestServer.service.germ.GermplasmService; import org.springframework.stereotype.Component; +import java.util.ArrayList; import java.util.List; @Component @@ -30,7 +34,33 @@ public Class getSearchRequestClass() { @Override public List> search( ListDetails list, - GermplasmListSearchRequest searchRequest) { - return germplasmService.searchGermplasmByList(list, searchRequest); + GermplasmListSearchRequest searchRequest, + Metadata metadata) throws BrAPIServerException { + + List results = germplasmService.getGermplasmInList(); + + List> listItems = new ArrayList<>(); + + for (Object[] result : results) { + ListItemWithFullItem item = new ListItemWithFullItem<>(); + + ListItemEntity listItemEntity = (ListItemEntity) result[0]; + GermplasmEntity germplasmEntity = (GermplasmEntity) result[1]; + + ListItem listItem = new ListItem(); + listItem.setListItemDbId(listItemEntity.getId().toString()); + listItem.setItem(listItemEntity.getItem()); + listItem.setPosition(listItemEntity.getPosition()); + + item.setlistItem(listItem); + + Germplasm germplasm = germplasmService.convertFromEntity(germplasmEntity); + + item.setItem(germplasm); + + listItems.add(item); + } + + return listItems; } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java b/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java index ef0fe896..12e988ed 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java @@ -1,10 +1,10 @@ package org.brapi.test.BrAPITestServer.handlers; +import io.swagger.model.Metadata; import io.swagger.model.core.ListDetails; import io.swagger.model.core.ListItemWithFullItem; -import io.swagger.model.core.ListItemsListResponseResult; import io.swagger.model.core.ListTypes; -import org.brapi.test.BrAPITestServer.model.entity.core.ListEntity; +import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; import java.util.List; @@ -15,6 +15,7 @@ public interface ListItemSearchRequestHandler { List> search( ListDetails list, - T searchRequest - ); + T searchRequest, + Metadata metadata + ) throws BrAPIServerException; } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepository.java b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepository.java index dd76d8f4..732924f5 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepository.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepository.java @@ -27,10 +27,12 @@ public interface BrAPIRepository S save(S entity); public List saveAll(Iterable entities); - + public void refresh(S entity); public void fetchXrefs(List page, Page pagedEntities, Class searchClass) throws InvalidPagingException; List findByIdIn(List ids); + + List testMappedEntityHql(SearchQueryBuilder searchQuery); } \ No newline at end of file diff --git a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java index 583a28cf..30a4fdfb 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java @@ -248,4 +248,17 @@ private void validatePageNumber(Pageable pageReq, Long totalCount) throws Invali throw new InvalidPagingException(); } } + + public List testMappedEntityHql(SearchQueryBuilder searchQuery) { + searchQuery.findGermplasmListItems(); + TypedQuery query = entityManager.createQuery(searchQuery.testHql, Object[].class); + + for (Entry entry : searchQuery.getParams().entrySet()) { + query.setParameter(entry.getKey(), entry.getValue()); + } + + query.setFirstResult((int) 0); + query.setMaxResults(10); + return query.getResultList(); + } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index 855aaf4c..2d34a853 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -8,6 +8,7 @@ import io.swagger.model.FilterBy; import io.swagger.model.GeoJSONSearchArea; +import io.swagger.model.core.ListTypes; import io.swagger.model.sort.SortBy; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; @@ -27,6 +28,7 @@ public class SearchQueryBuilder { private List joinedFetchedTables = new ArrayList<>(); private Class clazz; private final String default_date = "1970-01-01"; + public String testHql; public SearchQueryBuilder(Class clazz) { this.selectClause = "SELECT distinct entity FROM " + clazz.getSimpleName() + " entity "; @@ -528,4 +530,28 @@ private void joinCollectionColumn(String submittedSortFilterColumnName) { this.join(joinTableName, joinTableName); } + + public SearchQueryBuilder findGermplasmListItems() { + + testHql = """ + SELECT listItem, germplasm + FROM ListItemEntity listItem + JOIN listItem.list list + JOIN GermplasmEntity germplasm + ON germplasm.germplasmName = listItem.item + JOIN germplasm.pedigree pedigree + WHERE list.id = :listDbId + AND list.listType = :listType + AND germplasm.defaultDisplayName LIKE :defaultDisplayName + AND to_char(germplasm.createdDate, 'YYYY-MM-DD') LIKE :createdDate + ORDER BY germplasm.accessionNumber ASC, listItem.id ASC + """; + + params.put("listDbId", UUID.fromString("d200d581-1136-41e0-820d-806bf9cb63a8")); + params.put("listType", ListTypes.GERMPLASM); + params.put("defaultDisplayName", "%" + "86-MO-1-1.1" + "%"); + params.put("createdDate", "%20%"); + + return this; + } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java index d1f17c3c..bddd7a40 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java @@ -1,11 +1,13 @@ package org.brapi.test.BrAPITestServer.service.core; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import io.swagger.model.Metadata; import io.swagger.model.core.*; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; import org.brapi.test.BrAPITestServer.handlers.ListItemSearchRequestHandler; -import org.brapi.test.BrAPITestServer.model.entity.core.ListEntity; import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.stereotype.Service; import java.util.List; @@ -32,7 +34,8 @@ public ListItemService(ListService listService, } public List> search(String listDbId, - ListItemSearchRequest searchRequest) throws BrAPIServerException { + JsonNode searchRequest, + Metadata metadata) throws BrAPIServerException { ListDetails list = listService.getList(listDbId); ListItemSearchRequestHandler handler = handlers.get(list.getListType()); @@ -41,19 +44,30 @@ public List> search(String listDbId, throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "Unsupported list type: " + list.getListType()); } - return executeHandler(handler, list, searchRequest); + return executeHandler(handler, list, searchRequest, metadata); } - private List> executeHandler( - ListItemSearchRequestHandler handler, - ListDetails list, - ListItemSearchRequest request) { + private List> executeHandler(ListItemSearchRequestHandler handler, + ListDetails list, + JsonNode request, + Metadata metadata) throws BrAPIServerException { - T typedSearchRequest = objectMapper.convertValue( - request.getSearchRequest(), - handler.getSearchRequestClass() - ); + T typedSearchRequest; - return handler.search(list, typedSearchRequest); + try { + typedSearchRequest = objectMapper.convertValue( + request, + handler.getSearchRequestClass() + ); + } catch (IllegalArgumentException e) { + // This allows us to emulate the 400 request failures that normally propagate to the BrapiExceptionHandler.handleHttpMessageNotReadable() + // This is needed because of the generic nature of the EntityListSearchRequest objects, which come in as untyped JsonNode objects. + // To support EntityListSearchRequest objects of any type, the jackson serialization has to be done after where it normally occurs at the filter level. + // Throwing this type of exception obfuscates this fact, and if there are serialization/parsing issues we should see the same error messages we would normally see + // if done at the filter level. + throw new HttpMessageNotReadableException("Could not convert request object to JSON object.", e); + } + + return handler.search(list, typedSearchRequest, metadata); } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java index c5ac8e80..33510a5d 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java @@ -527,7 +527,7 @@ public List saveGermplasm(@Valid List body) thro .collect(Collectors.toList()); } - private Germplasm convertFromEntity(GermplasmEntity entity) { + public Germplasm convertFromEntity(GermplasmEntity entity) { Germplasm germ = new Germplasm(); UpdateUtility.convertFromEntity(entity, germ); @@ -1121,4 +1121,11 @@ private List getRedundantInstitutes( return dupInstitutes; } + public List getGermplasmInList() { + + SearchQueryBuilder searchQuery = new SearchQueryBuilder<>( + GermplasmEntity.class); + return germplasmRepository.testMappedEntityHql(searchQuery); + } + } From 9fd14f8e280a8b67a2f1799fcce99ab9443dda80 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 13 Oct 2026 18:12:09 -0400 Subject: [PATCH 03/15] Clean out sample code, bolster SearchQueryBuilder attributes --- .../GermplasmListSearchRequestHandler.java | 27 +------- .../primaryEntities/BrAPIRepository.java | 3 +- .../primaryEntities/BrAPIRepositoryImpl.java | 48 ++++++++++---- .../service/SearchQueryBuilder.java | 62 +++++++------------ .../service/germ/GermplasmService.java | 29 ++++++--- 5 files changed, 79 insertions(+), 90 deletions(-) diff --git a/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java b/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java index 957d83e0..718401bb 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java @@ -6,7 +6,6 @@ import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; import org.brapi.test.BrAPITestServer.model.entity.core.ListItemEntity; import org.brapi.test.BrAPITestServer.model.entity.germ.GermplasmEntity; -import org.brapi.test.BrAPITestServer.service.PagingUtility; import org.brapi.test.BrAPITestServer.service.germ.GermplasmService; import org.springframework.stereotype.Component; @@ -37,30 +36,6 @@ public List> search( GermplasmListSearchRequest searchRequest, Metadata metadata) throws BrAPIServerException { - List results = germplasmService.getGermplasmInList(); - - List> listItems = new ArrayList<>(); - - for (Object[] result : results) { - ListItemWithFullItem item = new ListItemWithFullItem<>(); - - ListItemEntity listItemEntity = (ListItemEntity) result[0]; - GermplasmEntity germplasmEntity = (GermplasmEntity) result[1]; - - ListItem listItem = new ListItem(); - listItem.setListItemDbId(listItemEntity.getId().toString()); - listItem.setItem(listItemEntity.getItem()); - listItem.setPosition(listItemEntity.getPosition()); - - item.setlistItem(listItem); - - Germplasm germplasm = germplasmService.convertFromEntity(germplasmEntity); - - item.setItem(germplasm); - - listItems.add(item); - } - - return listItems; + return germplasmService.searchGermplasmByList(list, searchRequest, metadata); } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepository.java b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepository.java index 732924f5..98ae8f25 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepository.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepository.java @@ -5,6 +5,7 @@ import java.util.Optional; import java.util.UUID; +import jakarta.persistence.Tuple; import org.brapi.test.BrAPITestServer.exceptions.InvalidPagingException; import org.brapi.test.BrAPITestServer.model.entity.BrAPIPrimaryEntity; import org.brapi.test.BrAPITestServer.service.SearchQueryBuilder; @@ -34,5 +35,5 @@ public interface BrAPIRepository findByIdIn(List ids); - List testMappedEntityHql(SearchQueryBuilder searchQuery); + public Page findAllBySearchAndPaginateMultipleEntitySelect(SearchQueryBuilder searchQuery, Pageable pageReq) throws InvalidPagingException; } \ No newline at end of file diff --git a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java index 30a4fdfb..1455d0e9 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java @@ -9,6 +9,7 @@ import java.util.stream.Collectors; import jakarta.persistence.EntityManager; +import jakarta.persistence.Tuple; import jakarta.persistence.TypedQuery; import org.brapi.test.BrAPITestServer.exceptions.InvalidPagingException; @@ -59,6 +60,23 @@ public Page findAllBySearchAndPaginate(SearchQueryBuilder searchQuery, Pag return new PageImpl<>(content, pageReq, totalCount); } + public Page findAllBySearchAndPaginateMultipleEntitySelect(SearchQueryBuilder searchQuery, Pageable pageReq) throws InvalidPagingException { + applyUserId(searchQuery); + + Long totalCount = getTotalCount(searchQuery); + + validatePageNumber(pageReq, totalCount); + + if (totalCount == 0) { + // Short-circuit to avoid running possible long-running query + return new PageImpl<>(Collections.emptyList(), pageReq, totalCount); + } + + List content = getPagedContentMultipleEntitySelect(searchQuery, pageReq); + + return new PageImpl<>(content, pageReq, totalCount); + } + /** * This method should be used when there is a need to page entities that are complex and have lots of lazily loaded, * one-to-many collection attributes. Call this method with the query you've built. @@ -181,6 +199,17 @@ private List getPagedContent(SearchQueryBuilder searchQuery, Pageable page return query.getResultList(); } + private List getPagedContentMultipleEntitySelect(SearchQueryBuilder searchQuery, Pageable pageReq) { + TypedQuery query = entityManager.createQuery(searchQuery.getQuery(), Tuple.class); + + setQueryParamsMultipleEntitySelect(query, searchQuery); + + query.setFirstResult((int) pageReq.getOffset()); + query.setMaxResults(pageReq.getPageSize()); + + return query.getResultList(); + } + private List searchEntitiesWithIds(SearchQueryBuilder searchQuery, List ids) { searchQuery.appendList(ids.stream().map(Object::toString).collect(Collectors.toList()), "id"); @@ -205,6 +234,12 @@ private void setQueryParams(TypedQuery query, SearchQueryBuilder searchQue } } + private void setQueryParamsMultipleEntitySelect(TypedQuery query, SearchQueryBuilder searchQuery) { + for (Entry entry : searchQuery.getParams().entrySet()) { + query.setParameter(entry.getKey(), entry.getValue()); + } + } + private List getPagedContentIdsOnly(SearchQueryBuilder searchQuery, Pageable pageReq) { TypedQuery query = entityManager.createQuery(searchQuery.getIdQuery(), UUID.class); @@ -248,17 +283,4 @@ private void validatePageNumber(Pageable pageReq, Long totalCount) throws Invali throw new InvalidPagingException(); } } - - public List testMappedEntityHql(SearchQueryBuilder searchQuery) { - searchQuery.findGermplasmListItems(); - TypedQuery query = entityManager.createQuery(searchQuery.testHql, Object[].class); - - for (Entry entry : searchQuery.getParams().entrySet()) { - query.setParameter(entry.getKey(), entry.getValue()); - } - - query.setFirstResult((int) 0); - query.setMaxResults(10); - return query.getResultList(); - } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index 2d34a853..843d4721 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -8,7 +8,6 @@ import io.swagger.model.FilterBy; import io.swagger.model.GeoJSONSearchArea; -import io.swagger.model.core.ListTypes; import io.swagger.model.sort.SortBy; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; @@ -18,8 +17,11 @@ public class SearchQueryBuilder { private String selectClause; + private String selectMultipleEntities; private String selectOnlyIds; private String selectOnlyIdsSubquery; + private String joinClause; + private String joinClauseIdSubquery; private String whereClause; private String defaultSort; private String sortClause; @@ -30,17 +32,19 @@ public class SearchQueryBuilder { private final String default_date = "1970-01-01"; public String testHql; - public SearchQueryBuilder(Class clazz) { - this.selectClause = "SELECT distinct entity FROM " + clazz.getSimpleName() + " entity "; + public SearchQueryBuilder(Class baseEntity) { + this.selectClause = "SELECT distinct entity FROM " + baseEntity.getSimpleName() + " entity "; // This is the top level query for selectOnlyIds. It is used to construct the full id query in getIdQuery() - this.selectOnlyIds = "SELECT entity.id FROM " + clazz.getSimpleName() + " entity "; + this.selectOnlyIds = "SELECT entity.id FROM " + baseEntity.getSimpleName() + " entity "; // This subquery will contain all the joins and filters necessary for the selectOnlyIds query and protects against duplicates using distinct - this.selectOnlyIdsSubquery = "SELECT distinct entity2.id FROM " + clazz.getSimpleName() + " entity2 "; + this.selectOnlyIdsSubquery = "SELECT distinct entity2.id FROM " + baseEntity.getSimpleName() + " entity2 "; + this.joinClause = ""; + this.joinClauseIdSubquery = ""; this.whereClause = "WHERE 1=1 "; this.defaultSort = " ORDER BY entity.id ASC "; this.sortClause = ""; this.params = new HashMap<>(); - this.clazz = clazz; + this.clazz = baseEntity; } public String getQuery() { @@ -49,7 +53,7 @@ public String getQuery() { sortClause = defaultSort; } - return selectClause + whereClause + sortClause; + return selectClause + joinClause + whereClause + sortClause; } /** @@ -88,7 +92,7 @@ public String getIdQuery() { String entityRegex = "(? withExRefs(List exRefIds, List exRe public SearchQueryBuilder join(String join, String name) { if (!this.joinedTables.contains(join) && !this.joinedFetchedTables.contains(join)) { - this.selectClause += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; - this.selectOnlyIdsSubquery += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; + this.joinClause += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; + this.joinClauseIdSubquery += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; this.joinedTables.add(join); } else if (joinedFetchedTables.contains(join) && !this.joinedTables.contains(join)) { - this.selectOnlyIdsSubquery += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; + this.joinClauseIdSubquery += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; } return this; } @@ -358,11 +362,11 @@ public SearchQueryBuilder leftJoinFetch(String join, String name, boolean ove if (this.joinedTables.contains(join) && overrideExistingJoin) { // Override existing normal join with join fetch if it already exists in a query // This override does not change the alias to the name provided, and assumes further usages will use the same alias. - this.selectClause = this.selectClause.replace("JOIN " + entityPrefix(join), "LEFT JOIN FETCH " + entityPrefix(join)); + this.joinClause = this.joinClause.replace("JOIN " + entityPrefix(join), "LEFT JOIN FETCH " + entityPrefix(join)); this.joinedFetchedTables.add(join); this.joinedTables.remove(join); } else if (!this.joinedFetchedTables.contains(join)) { - this.selectClause += generateLeftJoinFetch(join, name); + this.joinClause += generateLeftJoinFetch(join, name); this.joinedFetchedTables.add(join); } return this; @@ -377,8 +381,8 @@ public SearchQueryBuilder removeAndReplaceLeftJoinFetch(String join, String existingJoin, String existingName) { - this.selectClause = - this.selectClause.replace(generateLeftJoinFetch(existingJoin, existingName), generateLeftJoinFetch(join, name)); + this.joinClause = + this.joinClause.replace(generateLeftJoinFetch(existingJoin, existingName), generateLeftJoinFetch(join, name)); this.joinedFetchedTables.remove(existingJoin); this.joinedFetchedTables.add(join); @@ -391,8 +395,8 @@ public SearchQueryBuilder removeAndReplaceLeftJoinFetch(String join, * base query criteria to add another join fetch with leftJoinFetch() */ public SearchQueryBuilder removeLeftJoinFetch(String join, String name) { - this.selectClause = - this.selectClause.replace(generateLeftJoinFetch(join, name), ""); + this.joinClause = + this.joinClause.replace(generateLeftJoinFetch(join, name), ""); this.joinedFetchedTables.remove(join); return this; } @@ -530,28 +534,4 @@ private void joinCollectionColumn(String submittedSortFilterColumnName) { this.join(joinTableName, joinTableName); } - - public SearchQueryBuilder findGermplasmListItems() { - - testHql = """ - SELECT listItem, germplasm - FROM ListItemEntity listItem - JOIN listItem.list list - JOIN GermplasmEntity germplasm - ON germplasm.germplasmName = listItem.item - JOIN germplasm.pedigree pedigree - WHERE list.id = :listDbId - AND list.listType = :listType - AND germplasm.defaultDisplayName LIKE :defaultDisplayName - AND to_char(germplasm.createdDate, 'YYYY-MM-DD') LIKE :createdDate - ORDER BY germplasm.accessionNumber ASC, listItem.id ASC - """; - - params.put("listDbId", UUID.fromString("d200d581-1136-41e0-820d-806bf9cb63a8")); - params.put("listType", ListTypes.GERMPLASM); - params.put("defaultDisplayName", "%" + "86-MO-1-1.1" + "%"); - params.put("createdDate", "%20%"); - - return this; - } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java index 33510a5d..acbb2e54 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java @@ -7,8 +7,8 @@ import io.swagger.model.core.GermplasmListSearchRequest; import io.swagger.model.core.ListDetails; import io.swagger.model.core.ListItemWithFullItem; -import io.swagger.model.core.ProgramSearchRequest; import io.swagger.model.germ.*; +import jakarta.persistence.Tuple; import jakarta.validation.Valid; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerDbIdNotFoundException; @@ -33,7 +33,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; @@ -458,7 +457,7 @@ private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder> searchGermplasmByList(ListDetails list, GermplasmListSearchRequest request) { + public List> searchGermplasmByList(ListDetails list, GermplasmListSearchRequest request, Metadata metadata) { return new ArrayList<>(); } @@ -1121,11 +1120,23 @@ private List getRedundantInstitutes( return dupInstitutes; } - public List getGermplasmInList() { - - SearchQueryBuilder searchQuery = new SearchQueryBuilder<>( - GermplasmEntity.class); - return germplasmRepository.testMappedEntityHql(searchQuery); - } +// public List searchGermplasmByList(GermplasmListSearchRequest searchRequest, Metadata metadata) throws BrAPIServerException { +// +// Pageable pageReq = PagingUtility.getPageRequest(metadata); +// +// SearchQueryBuilder searchQuery = buildGermplasmSearchQuery(searchRequest); +// +// Page listItemAndGerm = germplasmRepository.findAllBySearchAndPaginateMultipleEntitySelect(searchQuery, pageReq); +// +// if (!listItemAndGerm.isEmpty()) { +// // Extract germs from page object, pass here +// // Ensure search query searches on germplasm and joins on list by id and type to get same results as above +// fetchRemainingGermCollectionsUsingQuery(searchQuery, listItemAndGerm); +// } +// +// SearchQueryBuilder searchQuery = new SearchQueryBuilder<>( +// GermplasmEntity.class); +// return germplasmRepository.testMappedEntityHql(searchQuery); +// } } From 87f96081b325068d2e76e9f9a784cab455e8aeb9 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Wed, 14 Oct 2026 18:34:03 -0400 Subject: [PATCH 04/15] Get /lists/{listDbId}/search/items into fully functional state --- .../java/io/swagger/model/SearchRequest.java | 18 +-- .../core/GermplasmListSearchRequest.java | 10 +- .../java/io/swagger/model/core/ListItem.java | 5 - .../model/core/ListItemWithFullItem.java | 4 +- .../model/core/StudySearchRequest.java | 38 ++--- .../model/core/TrialSearchRequest.java | 26 +-- .../model/germ/GermplasmSearchRequest.java | 30 ++-- .../GermplasmListSearchRequestHandler.java | 6 +- .../model/dto/EntityColumnNameAndType.java | 28 ---- .../dto/SortAndFilterEntityColumnInfo.java | 48 ++++++ .../primaryEntities/BrAPIRepositoryImpl.java | 16 +- .../service/SearchQueryBuilder.java | 74 ++++++--- .../service/core/ListItemService.java | 1 + .../service/core/StudyService.java | 4 +- .../service/core/TrialService.java | 4 +- .../service/germ/GermplasmService.java | 150 ++++++++++++++---- 16 files changed, 307 insertions(+), 155 deletions(-) delete mode 100644 src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityColumnNameAndType.java create mode 100644 src/main/java/org/brapi/test/BrAPITestServer/model/dto/SortAndFilterEntityColumnInfo.java diff --git a/src/main/java/io/swagger/model/SearchRequest.java b/src/main/java/io/swagger/model/SearchRequest.java index b59090fe..7999746f 100644 --- a/src/main/java/io/swagger/model/SearchRequest.java +++ b/src/main/java/io/swagger/model/SearchRequest.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.model.sort.SortBy; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; -import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; +import org.brapi.test.BrAPITestServer.model.dto.SortAndFilterEntityColumnInfo; import org.brapi.test.BrAPITestServer.model.dto.EntityType; import org.springframework.http.HttpStatus; @@ -150,7 +150,7 @@ public void setFilterBy(List filterBy) throws BrAPIServerException { return; } - Map allowedSortFilterNames = getEntityColAndTypeBySubmittedNameMap(); + Map allowedSortFilterNames = getEntityColInfoBySubmittedNameMap(); for (FilterBy filterByItem : filterBy) { @@ -162,17 +162,17 @@ public void setFilterBy(List filterBy) throws BrAPIServerException { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "value attribute not provided in element of filterBy list."); } - EntityColumnNameAndType entityColumnNameAndType = allowedSortFilterNames.get(filterByItem.getFilterOn()); + SortAndFilterEntityColumnInfo sortAndFilterEntityColumnInfo = allowedSortFilterNames.get(filterByItem.getFilterOn()); - if (entityColumnNameAndType == null) { + if (sortAndFilterEntityColumnInfo == null) { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, String.format("Supplied filterColumn [%s] not available in allowed names [%s]", filterByItem.getFilterOn(), allowedSortFilterNames.keySet()) ); } - if (entityColumnNameAndType.getEntityType() == EntityType.BOOLEAN) { + if (sortAndFilterEntityColumnInfo.getEntityType() == EntityType.BOOLEAN) { // TODO: Add support for this when it becomes relevant for BI or when there is time. - throw new BrAPIServerException(HttpStatus.BAD_REQUEST, String.format("Filtering not implemented for column name [%s] with associated data type [%s]", entityColumnNameAndType.getEntityColumnName(), EntityType.BOOLEAN)); + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, String.format("Filtering not implemented for column name [%s] with associated data type [%s]", sortAndFilterEntityColumnInfo.getEntityColumnName(), EntityType.BOOLEAN)); } } this.filterBy = filterBy; @@ -188,14 +188,14 @@ public void setSortBy(List sortBy) throws BrAPIServerException { return; } - Map allowedSortFilterNames = getEntityColAndTypeBySubmittedNameMap(); + Map allowedSortFilterNames = getEntityColInfoBySubmittedNameMap(); for (SortBy sortByItem : sortBy) { if (sortByItem.getSortedOn() == null || sortByItem.getSortedOn().isEmpty()) { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "sortedOn attribute not provided in element of sortBy list"); } - EntityColumnNameAndType sortColumnEntityNameAndType = getEntityColAndTypeBySubmittedNameMap().get(sortByItem.getSortedOn()); + SortAndFilterEntityColumnInfo sortColumnEntityNameAndType = getEntityColInfoBySubmittedNameMap().get(sortByItem.getSortedOn()); if (sortColumnEntityNameAndType == null) { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, @@ -208,7 +208,7 @@ public void setSortBy(List sortBy) throws BrAPIServerException { } @JsonIgnore - public Map getEntityColAndTypeBySubmittedNameMap() throws BrAPIServerException { + public Map getEntityColInfoBySubmittedNameMap() throws BrAPIServerException { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, String.format("Sort/Filtering not implemented for %s", this.getClass().getSimpleName())); } } diff --git a/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java b/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java index 2d50ac7c..b578b30c 100644 --- a/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java +++ b/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java @@ -1,7 +1,7 @@ package io.swagger.model.core; import io.swagger.model.germ.GermplasmSearchRequest; -import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; +import org.brapi.test.BrAPITestServer.model.dto.SortAndFilterEntityColumnInfo; import org.brapi.test.BrAPITestServer.model.dto.EntityType; import java.util.Collections; @@ -11,23 +11,23 @@ public class GermplasmListSearchRequest extends GermplasmSearchRequest { // The only real difference in this request is that there's an extra sort/filter field that GermplasmSearchRequest doesn't have. // It is specific to lists. - private static final Map + private static final Map LIST_ALLOWED_FIELDS; static { - Map fields = + Map fields = copyAllowedSortAndFilterFields(); fields.put( "listItemPosition", - new EntityColumnNameAndType("listItem.position", EntityType.NUMBER) + new SortAndFilterEntityColumnInfo("listItem.position", EntityType.NUMBER) ); LIST_ALLOWED_FIELDS = Collections.unmodifiableMap(fields); } @Override - public Map getEntityColAndTypeBySubmittedNameMap() { + public Map getEntityColInfoBySubmittedNameMap() { return LIST_ALLOWED_FIELDS; } diff --git a/src/main/java/io/swagger/model/core/ListItem.java b/src/main/java/io/swagger/model/core/ListItem.java index 0627eb7b..a3f70dd2 100644 --- a/src/main/java/io/swagger/model/core/ListItem.java +++ b/src/main/java/io/swagger/model/core/ListItem.java @@ -21,11 +21,6 @@ public class ListItem { @JsonProperty("item") private String item = null; - public ListItem listDbId(String listDbId) { - this.listItemDbId = listDbId; - return this; - } - /** * The unique identifier for a List * @return listDbId diff --git a/src/main/java/io/swagger/model/core/ListItemWithFullItem.java b/src/main/java/io/swagger/model/core/ListItemWithFullItem.java index bacfb5bf..322b43ef 100644 --- a/src/main/java/io/swagger/model/core/ListItemWithFullItem.java +++ b/src/main/java/io/swagger/model/core/ListItemWithFullItem.java @@ -24,11 +24,11 @@ public class ListItemWithFullItem { @ApiModelProperty(example = "6f621cfa", required = true, value = "The unique identifier for a List") - public ListItem getlistItem() { + public ListItem getListItem() { return listItem; } - public void setlistItem(ListItem listItem) { + public void setListItem(ListItem listItem) { this.listItem = listItem; } diff --git a/src/main/java/io/swagger/model/core/StudySearchRequest.java b/src/main/java/io/swagger/model/core/StudySearchRequest.java index ce9b0376..26d5f223 100644 --- a/src/main/java/io/swagger/model/core/StudySearchRequest.java +++ b/src/main/java/io/swagger/model/core/StudySearchRequest.java @@ -6,7 +6,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.model.SearchRequest; -import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; +import org.brapi.test.BrAPITestServer.model.dto.SortAndFilterEntityColumnInfo; import org.brapi.test.BrAPITestServer.model.dto.EntityType; import java.util.ArrayList; @@ -16,24 +16,24 @@ public class StudySearchRequest extends SearchRequest { // Key - allowed sort or field filter name for this entity // Value = entity field name and type that represents the submitted field. Used later on in query building. - private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = + private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = Map.ofEntries( - Map.entry("commonCropName", new EntityColumnNameAndType("crop.cropName", EntityType.TEXT)), - Map.entry("externalReferenceId", new EntityColumnNameAndType("*externalReferences.externalReferenceId", EntityType.TEXT)), - Map.entry("externalReferenceSource", new EntityColumnNameAndType("*externalReferences.externalReferenceSource", EntityType.TEXT)), - Map.entry("germplasmDbId", new EntityColumnNameAndType("*obsunit.germplasm.id", EntityType.UUID)), - Map.entry("locationDbId", new EntityColumnNameAndType("location.id", EntityType.UUID)), - Map.entry("observationVariableDbId", new EntityColumnNameAndType("*observation.observationVariable.id", EntityType.UUID)), - Map.entry("programDbId", new EntityColumnNameAndType("trial.program.id", EntityType.UUID)), - Map.entry("programName", new EntityColumnNameAndType("trial.program.name", EntityType.TEXT)), - Map.entry("seasonDbId", new EntityColumnNameAndType("*season.id", EntityType.UUID)), - Map.entry("studyCode", new EntityColumnNameAndType("studyCode", EntityType.TEXT)), - Map.entry("studyDbId", new EntityColumnNameAndType("id", EntityType.UUID)), - Map.entry("studyLocation", new EntityColumnNameAndType("location.id", EntityType.UUID)), - Map.entry("studyPUI", new EntityColumnNameAndType("studyPUI", EntityType.TEXT)), - Map.entry("trialDbId", new EntityColumnNameAndType("trial.id", EntityType.UUID)), - Map.entry("studyType", new EntityColumnNameAndType("studyType", EntityType.TEXT)), - Map.entry("studyName", new EntityColumnNameAndType("studyName", EntityType.TEXT)) + Map.entry("commonCropName", new SortAndFilterEntityColumnInfo("crop.cropName", EntityType.TEXT)), + Map.entry("externalReferenceId", new SortAndFilterEntityColumnInfo("*externalReferences.externalReferenceId", EntityType.TEXT)), + Map.entry("externalReferenceSource", new SortAndFilterEntityColumnInfo("*externalReferences.externalReferenceSource", EntityType.TEXT)), + Map.entry("germplasmDbId", new SortAndFilterEntityColumnInfo("*obsunit.germplasm.id", EntityType.UUID)), + Map.entry("locationDbId", new SortAndFilterEntityColumnInfo("location.id", EntityType.UUID)), + Map.entry("observationVariableDbId", new SortAndFilterEntityColumnInfo("*observation.observationVariable.id", EntityType.UUID)), + Map.entry("programDbId", new SortAndFilterEntityColumnInfo("trial.program.id", EntityType.UUID)), + Map.entry("programName", new SortAndFilterEntityColumnInfo("trial.program.name", EntityType.TEXT)), + Map.entry("seasonDbId", new SortAndFilterEntityColumnInfo("*season.id", EntityType.UUID)), + Map.entry("studyCode", new SortAndFilterEntityColumnInfo("studyCode", EntityType.TEXT)), + Map.entry("studyDbId", new SortAndFilterEntityColumnInfo("id", EntityType.UUID)), + Map.entry("studyLocation", new SortAndFilterEntityColumnInfo("location.id", EntityType.UUID)), + Map.entry("studyPUI", new SortAndFilterEntityColumnInfo("studyPUI", EntityType.TEXT)), + Map.entry("trialDbId", new SortAndFilterEntityColumnInfo("trial.id", EntityType.UUID)), + Map.entry("studyType", new SortAndFilterEntityColumnInfo("studyType", EntityType.TEXT)), + Map.entry("studyName", new SortAndFilterEntityColumnInfo("studyName", EntityType.TEXT)) ); @JsonProperty("commonCropNames") @@ -586,7 +586,7 @@ public Integer getTotalParameterCount() { } @Override - public Map getEntityColAndTypeBySubmittedNameMap() { + public Map getEntityColInfoBySubmittedNameMap() { return ALLOWED_SORT_AND_FILTER_FIELDS; } } diff --git a/src/main/java/io/swagger/model/core/TrialSearchRequest.java b/src/main/java/io/swagger/model/core/TrialSearchRequest.java index 2760e5d5..c4f1c293 100644 --- a/src/main/java/io/swagger/model/core/TrialSearchRequest.java +++ b/src/main/java/io/swagger/model/core/TrialSearchRequest.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.model.SearchRequest; -import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; +import org.brapi.test.BrAPITestServer.model.dto.SortAndFilterEntityColumnInfo; import org.brapi.test.BrAPITestServer.model.dto.EntityType; import java.time.LocalDate; @@ -13,18 +13,18 @@ public class TrialSearchRequest extends SearchRequest { // Key - allowed sort or field filter name for this entity // Value = entity field name and type that represents the submitted field. Used later on in query building. - private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = + private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = Map.of( - "trialName", new EntityColumnNameAndType("trialName", EntityType.TEXT), - "createdDate", new EntityColumnNameAndType("createdDate", EntityType.TEXT), - "createdBy", new EntityColumnNameAndType("createdBy", EntityType.TEXT), - "trialDbId", new EntityColumnNameAndType("id", EntityType.UUID), - "programDbId",new EntityColumnNameAndType("program.id", EntityType.UUID), - "startDate", new EntityColumnNameAndType("startDate", EntityType.TEXT), - "endDate", new EntityColumnNameAndType("endDate", EntityType.TEXT), - "active", new EntityColumnNameAndType("active", EntityType.BOOLEAN), - "programName", new EntityColumnNameAndType("program.name", EntityType.TEXT), - "locationDbId", new EntityColumnNameAndType("*studies.location.id", EntityType.UUID) + "trialName", new SortAndFilterEntityColumnInfo("trialName", EntityType.TEXT), + "createdDate", new SortAndFilterEntityColumnInfo("createdDate", EntityType.TEXT), + "createdBy", new SortAndFilterEntityColumnInfo("createdBy", EntityType.TEXT), + "trialDbId", new SortAndFilterEntityColumnInfo("id", EntityType.UUID), + "programDbId",new SortAndFilterEntityColumnInfo("program.id", EntityType.UUID), + "startDate", new SortAndFilterEntityColumnInfo("startDate", EntityType.TEXT), + "endDate", new SortAndFilterEntityColumnInfo("endDate", EntityType.TEXT), + "active", new SortAndFilterEntityColumnInfo("active", EntityType.BOOLEAN), + "programName", new SortAndFilterEntityColumnInfo("program.name", EntityType.TEXT), + "locationDbId", new SortAndFilterEntityColumnInfo("*studies.location.id", EntityType.UUID) ); @JsonProperty("commonCropNames") @@ -444,7 +444,7 @@ public Integer getTotalParameterCount() { } @Override - public Map getEntityColAndTypeBySubmittedNameMap() { + public Map getEntityColInfoBySubmittedNameMap() { return ALLOWED_SORT_AND_FILTER_FIELDS; } } diff --git a/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java b/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java index 029ab807..957e7465 100644 --- a/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java +++ b/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.model.SearchRequest; -import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; +import org.brapi.test.BrAPITestServer.model.dto.SortAndFilterEntityColumnInfo; import org.brapi.test.BrAPITestServer.model.dto.EntityType; import java.util.*; @@ -12,23 +12,23 @@ public class GermplasmSearchRequest extends SearchRequest { // Key - allowed sort or field filter name for this entity // Value = entity field name and type that represents the submitted field. Used later on in query building. - private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = + private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = Map.ofEntries( // accessionNumber = GID for DeltaBreed - Map.entry("accessionNumber", new EntityColumnNameAndType("accessionNumber", EntityType.NUMBER)), - Map.entry("defaultDisplayName", new EntityColumnNameAndType("defaultDisplayName", EntityType.TEXT)), - Map.entry("breedingMethod", new EntityColumnNameAndType("methodName", EntityType.TEXT)), - Map.entry("seedSource", new EntityColumnNameAndType("seedSource", EntityType.TEXT)), - Map.entry("pedigree",new EntityColumnNameAndType("pedigree.pedigreeString", EntityType.TEXT)), - Map.entry("femaleParentGID", new EntityColumnNameAndType("femaleParentGid", EntityType.TEXT)), - Map.entry("maleParentGID", new EntityColumnNameAndType("maleParentGid", EntityType.TEXT)), - Map.entry("createdDate", new EntityColumnNameAndType("createdDate", EntityType.DATE)), - Map.entry("createdBy", new EntityColumnNameAndType("createdBy", EntityType.TEXT)), - Map.entry("importEntryNumber", new EntityColumnNameAndType("importEntryNumber", EntityType.NUMBER)), - Map.entry("synonyms", new EntityColumnNameAndType("*synonyms.synonym", EntityType.TEXT)) + Map.entry("accessionNumber", new SortAndFilterEntityColumnInfo("accessionNumber", EntityType.NUMBER)), + Map.entry("defaultDisplayName", new SortAndFilterEntityColumnInfo("defaultDisplayName", EntityType.TEXT)), + Map.entry("breedingMethod", new SortAndFilterEntityColumnInfo("methodName", EntityType.TEXT)), + Map.entry("seedSource", new SortAndFilterEntityColumnInfo("seedSource", EntityType.TEXT)), + Map.entry("pedigree",new SortAndFilterEntityColumnInfo("pedigree.pedigreeString", EntityType.TEXT)), + Map.entry("femaleParentGID", new SortAndFilterEntityColumnInfo("femaleParentGid", EntityType.TEXT)), + Map.entry("maleParentGID", new SortAndFilterEntityColumnInfo("maleParentGid", EntityType.TEXT)), + Map.entry("createdDate", new SortAndFilterEntityColumnInfo("createdDate", EntityType.DATE)), + Map.entry("createdBy", new SortAndFilterEntityColumnInfo("createdBy", EntityType.TEXT)), + Map.entry("importEntryNumber", new SortAndFilterEntityColumnInfo("*listItem.position", EntityType.NUMBER, false, true)), + Map.entry("synonyms", new SortAndFilterEntityColumnInfo("*synonyms.synonym", EntityType.TEXT, true, false)) ); - protected static Map copyAllowedSortAndFilterFields() { + protected static Map copyAllowedSortAndFilterFields() { return new HashMap<>(ALLOWED_SORT_AND_FILTER_FIELDS); } @@ -642,7 +642,7 @@ public Integer getTotalParameterCount() { } @Override - public Map getEntityColAndTypeBySubmittedNameMap() { + public Map getEntityColInfoBySubmittedNameMap() { return ALLOWED_SORT_AND_FILTER_FIELDS; } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java b/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java index 718401bb..b1d731b7 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java @@ -2,14 +2,10 @@ import io.swagger.model.Metadata; import io.swagger.model.core.*; -import io.swagger.model.germ.Germplasm; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; -import org.brapi.test.BrAPITestServer.model.entity.core.ListItemEntity; -import org.brapi.test.BrAPITestServer.model.entity.germ.GermplasmEntity; import org.brapi.test.BrAPITestServer.service.germ.GermplasmService; import org.springframework.stereotype.Component; -import java.util.ArrayList; import java.util.List; @Component @@ -36,6 +32,6 @@ public List> search( GermplasmListSearchRequest searchRequest, Metadata metadata) throws BrAPIServerException { - return germplasmService.searchGermplasmByList(list, searchRequest, metadata); + return germplasmService.findGermplasmByList(list, searchRequest, metadata); } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityColumnNameAndType.java b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityColumnNameAndType.java deleted file mode 100644 index 71f4a9f2..00000000 --- a/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityColumnNameAndType.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.brapi.test.BrAPITestServer.model.dto; - -/** - * This class is used to map an entity's column name to the SQL type of lookup that should be completed for filter searches. - * The entityColumnName is also used for sorts as well. - */ -public class EntityColumnNameAndType { - String entityColumnName; - EntityType entityType; - - public EntityColumnNameAndType(String entityColumnName, EntityType entityType) { - this.entityColumnName = entityColumnName; - this.entityType = entityType; - } - - public String getEntityColumnName() { - return entityColumnName; - } - - public EntityType getEntityType() { - return entityType; - } - - @Override - public String toString() { - return this.entityColumnName; - } -} diff --git a/src/main/java/org/brapi/test/BrAPITestServer/model/dto/SortAndFilterEntityColumnInfo.java b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/SortAndFilterEntityColumnInfo.java new file mode 100644 index 00000000..8fbf43e1 --- /dev/null +++ b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/SortAndFilterEntityColumnInfo.java @@ -0,0 +1,48 @@ +package org.brapi.test.BrAPITestServer.model.dto; + +/** + * This class is used to map an entity's column name to the SQL type of lookup that should be completed for filter searches. + * The entityColumnName is also used for sorts as well. + */ +public class SortAndFilterEntityColumnInfo { + private final String entityColumnName; + private final EntityType entityType; + private final boolean isCollectionOnEntity; + private final boolean isJoinOnUnrelatedEntity; + + + public SortAndFilterEntityColumnInfo(String entityColumnName, EntityType entityType) { + this.entityColumnName = entityColumnName; + this.entityType = entityType; + this.isCollectionOnEntity = false; + this.isJoinOnUnrelatedEntity = false; + } + + public SortAndFilterEntityColumnInfo(String entityColumnName, EntityType entityType, boolean isCollectionOnEntity, boolean isJoinOnUnrelatedEntity) { + this.entityColumnName = entityColumnName; + this.entityType = entityType; + this.isCollectionOnEntity = isCollectionOnEntity; + this.isJoinOnUnrelatedEntity = isJoinOnUnrelatedEntity; + } + + public String getEntityColumnName() { + return entityColumnName; + } + + public EntityType getEntityType() { + return entityType; + } + + public boolean isCollectionOnEntity() { + return isCollectionOnEntity; + } + + public boolean isJoinOnUnrelatedEntity() { + return isJoinOnUnrelatedEntity; + } + + @Override + public String toString() { + return this.entityColumnName; + } +} diff --git a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java index 1455d0e9..52d0f081 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java @@ -63,7 +63,7 @@ public Page findAllBySearchAndPaginate(SearchQueryBuilder searchQuery, Pag public Page findAllBySearchAndPaginateMultipleEntitySelect(SearchQueryBuilder searchQuery, Pageable pageReq) throws InvalidPagingException { applyUserId(searchQuery); - Long totalCount = getTotalCount(searchQuery); + Long totalCount = getMultipleSelectCountQuery(searchQuery); validatePageNumber(pageReq, totalCount); @@ -91,6 +91,7 @@ public Page findAllBySearchAndPaginateMultipleEntitySelect(SearchQueryBui * If the need is to fetch according to an entirely different base criteria, feel free to call this method again. * * The results will be paged and ordered based on the submitted searchQuery and pageReq. + * TODO: This method and it's confusing use of SearchQueryBuilder.idQuery may not be necessary at all if we keep left join fetches out of initial queries and fetch after. */ public Page findAllBySearchPaginatingWithFetches(SearchQueryBuilder searchQuery, Pageable pageReq) throws InvalidPagingException { @@ -255,6 +256,19 @@ private List getPagedContentIdsOnly(SearchQueryBuilder searchQuery, Pag } + private Long getMultipleSelectCountQuery(SearchQueryBuilder searchQuery) + { + TypedQuery query = entityManager.createQuery(searchQuery.getCountQueryMultipleSelect(), Long.class); + for (Entry entry : searchQuery.getParams().entrySet()) { + query.setParameter(entry.getKey(), entry.getValue()); + } + + List content = query.getResultList(); + if (!content.isEmpty()) { + return content.getFirst(); + } + return 0L; + } private Long getTotalCount(SearchQueryBuilder searchQuery) { diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index 843d4721..bcd411ec 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -10,14 +10,13 @@ import io.swagger.model.GeoJSONSearchArea; import io.swagger.model.sort.SortBy; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; -import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; +import org.brapi.test.BrAPITestServer.model.dto.SortAndFilterEntityColumnInfo; import org.brapi.test.BrAPITestServer.model.dto.EntityType; import org.springframework.http.HttpStatus; public class SearchQueryBuilder { private String selectClause; - private String selectMultipleEntities; private String selectOnlyIds; private String selectOnlyIdsSubquery; private String joinClause; @@ -30,7 +29,6 @@ public class SearchQueryBuilder { private List joinedFetchedTables = new ArrayList<>(); private Class clazz; private final String default_date = "1970-01-01"; - public String testHql; public SearchQueryBuilder(Class baseEntity) { this.selectClause = "SELECT distinct entity FROM " + baseEntity.getSimpleName() + " entity "; @@ -56,6 +54,22 @@ public String getQuery() { return selectClause + joinClause + whereClause + sortClause; } + public String getCountQueryMultipleSelect() { + String firstSelection = selectClause + .replaceFirst("(?i)^SELECT\\s+(?:DISTINCT\\s+)?", "") + .split(",")[0] + .trim(); + + int fromIndex = selectClause.toUpperCase(Locale.ROOT).indexOf(" FROM "); + + String fromClause = selectClause.substring(fromIndex); + + return "SELECT COUNT(DISTINCT " + firstSelection + ".id) " + + fromClause + + joinClause.replaceAll("(?i)LEFT JOIN FETCH", "LEFT JOIN") + + whereClause; + } + /** * The goal of this query is to return a query that will return only the BrAPI dbIds of the source entity, applying * sorting, filtering, and pagination to the query. @@ -106,6 +120,13 @@ public Class getClazz() { return clazz; } + public SearchQueryBuilder modifySelect(List entityAliases) { + + this.selectClause = "SELECT distinct " + String.join(", ", entityAliases) + " FROM " + clazz.getSimpleName() + " entity "; + + return this; + } + public SearchQueryBuilder appendList(List list, String columnName) { String paramName = paramFilter(columnName); if (list != null && !list.isEmpty()) { @@ -353,6 +374,15 @@ public SearchQueryBuilder join(String join, String name) { return this; } + public SearchQueryBuilder joinEntity(Class joinedEntity, + String alias, + String onClause) { + this.joinClause += " JOIN " + joinedEntity.getSimpleName() + + " " + alias + + " ON " + onClause; + return this; + } + public SearchQueryBuilder leftJoinFetch(String join, String name) { return leftJoinFetch(join, name, false); } @@ -434,19 +464,19 @@ private String paramFilterPattern(String param) { * - A column name * - An order (DESC, ASC) */ - public SearchQueryBuilder sortBy(List sortBy, Map entityColAndTypeBySubmittedName) throws BrAPIServerException { + public SearchQueryBuilder sortBy(List sortBy, Map entityColInfoBySubmittedName) throws BrAPIServerException { if (sortBy == null || sortBy.isEmpty()) { return this; } for (SortBy sort : sortBy) { - // At this point, the submitted sortBy name has been verified to be in entityColAndTypeBySubmittedName - EntityColumnNameAndType entityColumnNameAndType = entityColAndTypeBySubmittedName.get(sort.getSortedOn()); + // At this point, the submitted sortBy name has been verified to be in entityColInfoBySubmittedName + SortAndFilterEntityColumnInfo sortAndFilterEntityColumnInfo = entityColInfoBySubmittedName.get(sort.getSortedOn()); - String entityColName = entityColumnNameAndType.getEntityColumnName(); + String entityColName = sortAndFilterEntityColumnInfo.getEntityColumnName(); - if (entityColName.startsWith("*")) { + if (sortAndFilterEntityColumnInfo.isCollectionOnEntity()) { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "Sorting on one to many relationships not supported"); } @@ -457,7 +487,8 @@ public SearchQueryBuilder sortBy(List sortBy, Map filterBy(List filterBy, Map entityColAndTypeBySubmittedName) throws BrAPIServerException { + public SearchQueryBuilder filterBy(List filterBy, Map entityColAndTypeBySubmittedName) throws BrAPIServerException { SearchQueryBuilder searchQuery = this; if (filterBy == null || filterBy.isEmpty()) { @@ -503,21 +534,22 @@ public SearchQueryBuilder filterBy(List filterBy, Map findStudies(StudySearchRequest request, Metadata metaData) .appendList(request.getStudyDbIds(), "id").appendList(request.getStudyNames(), "studyName") .appendList(request.getStudyPUIs(), "studyPUI").appendList(request.getStudyTypes(), "studyType") .appendList(request.getTrialDbIds(), "trial.id").appendList(request.getTrialNames(), "trial.trialName") - .sortBy(request.getSortByElements(), request.getEntityColAndTypeBySubmittedNameMap()) - .filterBy(request.getFilterBy(), request.getEntityColAndTypeBySubmittedNameMap()); + .sortBy(request.getSortByElements(), request.getEntityColInfoBySubmittedNameMap()) + .filterBy(request.getFilterBy(), request.getEntityColInfoBySubmittedNameMap()); Page studiesPage = studyRepository.findAllBySearchAndPaginate(searchQuery, pageReq); PagingUtility.calculateMetaData(metaData, studiesPage); diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java index 21c30c65..6c3fc54d 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java @@ -127,8 +127,8 @@ public List findTrials(@Valid TrialSearchRequest request, Metadata metada .appendList(request.getStudyNames(), "*studies.studyName").appendList(request.getTrialDbIds(), "id") .appendList(request.getTrialNames(), "trialName") .appendDateRange(request.getSearchDateRangeStart(), request.getSearchDateRangeEnd(), "startDate") - .sortBy(request.getSortByElements(), request.getEntityColAndTypeBySubmittedNameMap()) - .filterBy(request.getFilterBy(), request.getEntityColAndTypeBySubmittedNameMap()); + .sortBy(request.getSortByElements(), request.getEntityColInfoBySubmittedNameMap()) + .filterBy(request.getFilterBy(), request.getEntityColInfoBySubmittedNameMap()); Page trialsPage = trialRepository.findAllBySearchAndPaginate(searchQuery, pageReq); PagingUtility.calculateMetaData(metadata, trialsPage); diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java index acbb2e54..eea70c74 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java @@ -6,8 +6,11 @@ import io.swagger.model.core.GermplasmListSearchRequest; import io.swagger.model.core.ListDetails; +import io.swagger.model.core.ListItem; import io.swagger.model.core.ListItemWithFullItem; import io.swagger.model.germ.*; +import io.swagger.model.sort.SortBy; +import io.swagger.model.sort.SortOrder; import jakarta.persistence.Tuple; import jakarta.validation.Valid; @@ -16,6 +19,7 @@ import org.brapi.test.BrAPITestServer.model.entity.BrAPIBaseEntity; import org.brapi.test.BrAPITestServer.model.entity.ExternalReferenceEntity; import org.brapi.test.BrAPITestServer.model.entity.core.CropEntity; +import org.brapi.test.BrAPITestServer.model.entity.core.ListItemEntity; import org.brapi.test.BrAPITestServer.model.entity.core.ProgramEntity; import org.brapi.test.BrAPITestServer.model.entity.germ.*; import org.brapi.test.BrAPITestServer.model.entity.germ.GermplasmInstituteEntity.InstituteTypeEnum; @@ -140,6 +144,8 @@ public Page findGermplasmEntities(@Valid GermplasmSearchRequest if (!germs.isEmpty()) { // If records were found, iterate through the rest of the lazily loaded collections of the GermplasmEntity // and LEFT JOIN FETCH all of them. + log.debug("Fetching synonyms"); + fetchSynonyms(ids, germs); log.debug("Fetching xrefs"); fetchXrefs(ids, germs); log.debug("fetching attributes"); @@ -175,8 +181,7 @@ public List findGermplasmEntitiesWithoutPaging(@Valid Germplasm private SearchQueryBuilder buildGermplasmSearchQuery(GermplasmSearchRequest request) throws BrAPIServerException { SearchQueryBuilder searchQuery = new SearchQueryBuilder( GermplasmEntity.class); - searchQuery.leftJoinFetch("synonyms", "synonyms") - .leftJoinFetch("breedingMethod", "breedingMethod") + searchQuery.leftJoinFetch("breedingMethod", "breedingMethod") .leftJoinFetch("crop", "crop") .leftJoinFetch("pedigree", "pedigree") .leftJoinFetch("*pedigree.crossingProject", "crossingProject"); @@ -210,8 +215,8 @@ private SearchQueryBuilder buildGermplasmSearchQuery(GermplasmS .appendList(request.getGenus(), "genus").appendList(request.getSpecies(), "species") .appendNamesList(request.getBinomialNames(), "genus", "genus", "species") .appendList(request.getFamilyCodes(), "familyCode") - .filterBy(request.getFilterBy(), request.getEntityColAndTypeBySubmittedNameMap()) - .sortBy(request.getSortByElements(), request.getEntityColAndTypeBySubmittedNameMap()); + .filterBy(request.getFilterBy(), request.getEntityColInfoBySubmittedNameMap()) + .sortBy(request.getSortByElements(), request.getEntityColInfoBySubmittedNameMap()); return searchQuery; } @@ -220,6 +225,21 @@ private SearchQueryBuilder buildGermplasmSearchQuery(GermplasmS // TODO: Therefore no need to store a map and can just iterate through both at the same time O(n). // TODO: Revisit when sortBy is added to GermplasmSearchRequest and we can pass through a sortBy value. + + private void fetchSynonyms(List ids, Page germEntities) { + SearchQueryBuilder searchQuery = new SearchQueryBuilder<>(GermplasmEntity.class); + + searchQuery.leftJoinFetch("synonyms", "synonyms") + .appendIds(ids); + + List germsWithSynonyms = germplasmRepository.findAllBySearch(searchQuery); + + Map> synonymsByEntity = new HashMap<>(); + germsWithSynonyms.forEach(entity -> synonymsByEntity.put(entity.getId().toString(), entity.getSynonyms())); + + germEntities.forEach(entity -> entity.setSynonyms(synonymsByEntity.get(entity.getId().toString()))); + } + private void fetchXrefs(List ids, Page germEntities) { SearchQueryBuilder searchQuery = new SearchQueryBuilder<>(GermplasmEntity.class); @@ -339,16 +359,30 @@ private void fetchPedigreeEdges(List ids, Page germEntiti private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder searchQuery, List germEntities) { // Remove unused left join fetches from the base query - searchQuery.removeLeftJoinFetch("synonyms", "synonyms") - .removeLeftJoinFetch("breedingMethod", "breedingMethod") + searchQuery.removeLeftJoinFetch("breedingMethod", "breedingMethod") .removeLeftJoinFetch("crop", "crop") .removeLeftJoinFetch("pedigree", "pedigree") .removeLeftJoinFetch("*pedigree.crossingProject", "crossingProject") .resetSortClause(); + + // Fetch synonyms + log.debug("fetching synonyms"); + searchQuery.leftJoinFetch("synonyms", + "synonyms"); + + List synonyms = germplasmRepository.findAllBySearch(searchQuery); + + Map> synonymsByEntity = new HashMap<>(); + synonyms.forEach(entity -> synonymsByEntity.put(entity.getId().toString(), entity.getExternalReferences())); + + germEntities.forEach(entity -> entity.setExternalReferences(synonymsByEntity.get(entity.getId().toString()))); + // Fetch xrefs log.debug("Fetching xrefs"); - searchQuery.leftJoinFetch("externalReferences", - "externalReferences"); + searchQuery.removeAndReplaceLeftJoinFetch("externalReferences", + "externalReferences", + "synonyms", + "synonyms"); List xrefs = germplasmRepository.findAllBySearch(searchQuery); @@ -457,8 +491,14 @@ private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder> searchGermplasmByList(ListDetails list, GermplasmListSearchRequest request, Metadata metadata) { - return new ArrayList<>(); + public List> findGermplasmByList(ListDetails list, GermplasmListSearchRequest request, Metadata metadata) throws BrAPIServerException{ + + Page page = searchGermplasmByList(list, request, metadata); + + List> listItems = page.map(this::convertFromEntity).getContent(); + PagingUtility.calculateMetaData(metadata, page); + + return listItems; } public Germplasm getGermplasm(String germplasmDbId) throws BrAPIServerException { @@ -526,6 +566,23 @@ public List saveGermplasm(@Valid List body) thro .collect(Collectors.toList()); } + private ListItemWithFullItem convertFromEntity(Tuple listItemAndGermplasmEntity) { + ListItemWithFullItem listItemWithFullItem = new ListItemWithFullItem<>(); + + ListItem listItem = new ListItem(); + + ListItemEntity listItemEntity = listItemAndGermplasmEntity.get(0, ListItemEntity.class); + + listItem.setListItemDbId(listItemEntity.getId().toString()); + listItem.setPosition(listItemEntity.getPosition()); + listItem.setItem(listItemEntity.getItem()); + + listItemWithFullItem.setListItem(listItem); + listItemWithFullItem.setItem(convertFromEntity(listItemAndGermplasmEntity.get(1, GermplasmEntity.class))); + + return listItemWithFullItem; + } + public Germplasm convertFromEntity(GermplasmEntity entity) { Germplasm germ = new Germplasm(); UpdateUtility.convertFromEntity(entity, germ); @@ -1120,23 +1177,60 @@ private List getRedundantInstitutes( return dupInstitutes; } -// public List searchGermplasmByList(GermplasmListSearchRequest searchRequest, Metadata metadata) throws BrAPIServerException { -// -// Pageable pageReq = PagingUtility.getPageRequest(metadata); -// -// SearchQueryBuilder searchQuery = buildGermplasmSearchQuery(searchRequest); -// -// Page listItemAndGerm = germplasmRepository.findAllBySearchAndPaginateMultipleEntitySelect(searchQuery, pageReq); -// -// if (!listItemAndGerm.isEmpty()) { -// // Extract germs from page object, pass here -// // Ensure search query searches on germplasm and joins on list by id and type to get same results as above -// fetchRemainingGermCollectionsUsingQuery(searchQuery, listItemAndGerm); -// } -// -// SearchQueryBuilder searchQuery = new SearchQueryBuilder<>( -// GermplasmEntity.class); -// return germplasmRepository.testMappedEntityHql(searchQuery); -// } + public Page searchGermplasmByList(ListDetails list, GermplasmListSearchRequest searchRequest, Metadata metadata) throws BrAPIServerException { + + Pageable pageReq = PagingUtility.getPageRequest(metadata); + + List sortByElements = searchRequest.getSortByElements(); + + if (sortByElements == null || sortByElements.isEmpty()) { + // If there is no specified sort, sort by the position of the list. + SortBy defaultListItemSort = new SortBy("importEntryNumber", SortOrder.ASC); + searchRequest.setSortBy(List.of(defaultListItemSort)); + } + + SearchQueryBuilder searchQuery = buildGermplasmSearchQuery(searchRequest); + + searchQuery.modifySelect(List.of("listItem", "entity")); + + searchQuery.joinEntity(ListItemEntity.class, "listItem", " entity.germplasmName = listItem.item ") + .join("*listItem.list", "list") + .appendSingle(UUID.fromString(list.getListDbId()), "*list.id") + .appendEnum(list.getListType(), "*list.listType"); + + Page listItemAndGerm = germplasmRepository.findAllBySearchAndPaginateMultipleEntitySelect(searchQuery, pageReq); + + Page germs = listItemAndGerm.map( + // Germplasm is the second column of the Tuple + row -> (GermplasmEntity) row.get(1) + ); + + List ids = germs.get().map(GermplasmEntity::getId).toList(); + + if (!germs.isEmpty()) { + // If records were found, iterate through the rest of the lazily loaded collections of the GermplasmEntity + // and LEFT JOIN FETCH all of them. + log.debug("Fetching synonyms"); + fetchSynonyms(ids, germs); + log.debug("Fetching xrefs"); + fetchXrefs(ids, germs); + log.debug("fetching attributes"); + fetchAttributes(ids, germs); + log.debug("fetching donors"); + fetchDonors(ids, germs); + log.debug("fetching origins"); + fetchOrigin(ids, germs); + log.debug("fetching institutes"); + fetchInstitutes(ids, germs); + log.debug("fetching taxons"); + fetchTaxons(ids, germs); + log.debug("fetching storage codes"); + fetchStorageCodes(ids, germs); + log.debug("fetching pedigree edges"); + fetchPedigreeEdges(ids, germs); + } + + return listItemAndGerm; + } } From 3c08de58b65e54f65a4933ccd3e98afeded6e3bc Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Wed, 14 Oct 2026 18:56:17 -0400 Subject: [PATCH 05/15] Add @Formula for checking item position with 1 index --- .../model/core/GermplasmListSearchRequest.java | 2 +- .../model/entity/core/ListItemEntity.java | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java b/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java index b578b30c..6a1aeae8 100644 --- a/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java +++ b/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java @@ -20,7 +20,7 @@ public class GermplasmListSearchRequest extends GermplasmSearchRequest { fields.put( "listItemPosition", - new SortAndFilterEntityColumnInfo("listItem.position", EntityType.NUMBER) + new SortAndFilterEntityColumnInfo("*listItem.positionIndexedBy1", EntityType.NUMBER, false, true) ); LIST_ALLOWED_FIELDS = Collections.unmodifiableMap(fields); diff --git a/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/ListItemEntity.java b/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/ListItemEntity.java index 92b76da3..3b553823 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/ListItemEntity.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/ListItemEntity.java @@ -3,6 +3,7 @@ import jakarta.persistence.*; import org.brapi.test.BrAPITestServer.model.entity.BrAPIBaseEntity; +import org.hibernate.annotations.Formula; import org.hibernate.annotations.Where; @Entity @@ -17,6 +18,8 @@ public class ListItemEntity extends BrAPIBaseEntity { private Integer position; @Column(name = "soft_deleted") private boolean softDeleted; + @Formula("position + 1") + private Integer positionIndexedBy1; public ListEntity getList() { return list; @@ -38,4 +41,16 @@ public void setPosition(Integer position) { } public boolean getSoftDeleted() { return softDeleted; } public void setSoftDeleted(boolean softDeleted) { this.softDeleted = softDeleted; } + + public boolean isSoftDeleted() { + return softDeleted; + } + + public Integer getPositionIndexedBy1() { + return positionIndexedBy1; + } + + public void setPositionIndexedBy1(Integer positionIndexedBy1) { + this.positionIndexedBy1 = positionIndexedBy1; + } } From 6b906c9f6b5626b295f201e054f60a23a39ed798 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Wed, 14 Oct 2026 19:01:16 -0400 Subject: [PATCH 06/15] Fix GermplasmService redline in IDE not causing compile or runtime errors --- .../test/BrAPITestServer/service/germ/GermplasmService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java index eea70c74..0f786abb 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java @@ -495,10 +495,10 @@ public List> findGermplasmByList(ListDetails list, Germp Page page = searchGermplasmByList(list, request, metadata); - List> listItems = page.map(this::convertFromEntity).getContent(); + Page> listItems = page.map(this::convertFromEntity); PagingUtility.calculateMetaData(metadata, page); - return listItems; + return listItems.getContent(); } public Germplasm getGermplasm(String germplasmDbId) throws BrAPIServerException { From bb71d95a992c02bca41f0778c06e6d9a064276f0 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Thu, 15 Oct 2026 18:43:38 -0400 Subject: [PATCH 07/15] Model edits --- .../core/GermplasmListSearchRequest.java | 2 -- .../model/core/ListItemSearchRequest.java | 20 ------------------- .../model/core/ListItemWithFullItem.java | 2 +- .../model/core/ListItemsListResponse.java | 12 +++++------ .../core/ListItemsListResponseResult.java | 6 +++--- 5 files changed, 10 insertions(+), 32 deletions(-) delete mode 100644 src/main/java/io/swagger/model/core/ListItemSearchRequest.java diff --git a/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java b/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java index 6a1aeae8..2ece7db8 100644 --- a/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java +++ b/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java @@ -30,6 +30,4 @@ public class GermplasmListSearchRequest extends GermplasmSearchRequest { public Map getEntityColInfoBySubmittedNameMap() { return LIST_ALLOWED_FIELDS; } - - } diff --git a/src/main/java/io/swagger/model/core/ListItemSearchRequest.java b/src/main/java/io/swagger/model/core/ListItemSearchRequest.java deleted file mode 100644 index c5fe9b33..00000000 --- a/src/main/java/io/swagger/model/core/ListItemSearchRequest.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.swagger.model.core; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.JsonNode; - -public class ListItemSearchRequest { - - @JsonProperty("searchRequest") - private JsonNode searchRequest; - - // List-item-specific filters and sorting can go here. - - public JsonNode getSearchRequest() { - return searchRequest; - } - - public void setSearchRequest(JsonNode searchRequest) { - this.searchRequest = searchRequest; - } -} diff --git a/src/main/java/io/swagger/model/core/ListItemWithFullItem.java b/src/main/java/io/swagger/model/core/ListItemWithFullItem.java index 322b43ef..f091c0a1 100644 --- a/src/main/java/io/swagger/model/core/ListItemWithFullItem.java +++ b/src/main/java/io/swagger/model/core/ListItemWithFullItem.java @@ -48,7 +48,7 @@ public boolean equals(java.lang.Object o) { if (o == null || getClass() != o.getClass()) { return false; } - ListItemWithFullItem listItemWithFullItem = (ListItemWithFullItem) o; + ListItemWithFullItem listItemWithFullItem = (ListItemWithFullItem) o; return Objects.equals(this.listItem, listItemWithFullItem.listItem) && super.equals(o); } diff --git a/src/main/java/io/swagger/model/core/ListItemsListResponse.java b/src/main/java/io/swagger/model/core/ListItemsListResponse.java index e36f43e7..a5021c38 100644 --- a/src/main/java/io/swagger/model/core/ListItemsListResponse.java +++ b/src/main/java/io/swagger/model/core/ListItemsListResponse.java @@ -15,7 +15,7 @@ */ @Validated @javax.annotation.processing.Generated(value = "io.swagger.codegen.v3.generators.java.SpringCodegen", date = "2020-03-20T16:31:52.030Z[GMT]") -public class ListItemsListResponse implements BrAPIResponse { +public class ListItemsListResponse implements BrAPIResponse> { @JsonProperty("@context") private Context _atContext = null; @@ -23,7 +23,7 @@ public class ListItemsListResponse implements BrAPIResponse result = null; public ListItemsListResponse() { this._atContext = new Context(); @@ -56,7 +56,7 @@ public void setMetadata(Metadata metadata) { this.metadata = metadata; } - public ListItemsListResponse result(ListItemsListResponseResult result) { + public ListItemsListResponse result(ListItemsListResponseResult result) { this.result = result; return this; } @@ -70,11 +70,11 @@ public ListItemsListResponse result(ListItemsListResponseResult result) { @Valid - public ListItemsListResponseResult getResult() { + public ListItemsListResponseResult getResult() { return result; } - public void setResult(ListItemsListResponseResult result) { + public void setResult(ListItemsListResponseResult result) { this.result = result; } @@ -86,7 +86,7 @@ public boolean equals(java.lang.Object o) { if (o == null || getClass() != o.getClass()) { return false; } - ListItemsListResponse listItemsListResponse = (ListItemsListResponse) o; + ListItemsListResponse listItemsListResponse = (ListItemsListResponse) o; return Objects.equals(this._atContext, listItemsListResponse._atContext) && Objects.equals(this.metadata, listItemsListResponse.metadata) && Objects.equals(this.result, listItemsListResponse.result); diff --git a/src/main/java/io/swagger/model/core/ListItemsListResponseResult.java b/src/main/java/io/swagger/model/core/ListItemsListResponseResult.java index 6a5f6561..892a2dba 100644 --- a/src/main/java/io/swagger/model/core/ListItemsListResponseResult.java +++ b/src/main/java/io/swagger/model/core/ListItemsListResponseResult.java @@ -20,12 +20,12 @@ public class ListItemsListResponseResult implements BrAPIResponseResult> data = new ArrayList>(); - public ListItemsListResponseResult data(List> data) { + public ListItemsListResponseResult data(List> data) { this.data = data; return this; } - public ListItemsListResponseResult addDataItem(ListItemWithFullItem dataItem) { + public ListItemsListResponseResult addDataItem(ListItemWithFullItem dataItem) { this.data.add(dataItem); return this; } @@ -54,7 +54,7 @@ public boolean equals(java.lang.Object o) { if (o == null || getClass() != o.getClass()) { return false; } - ListItemsListResponseResult listItemsListResponseResult = (ListItemsListResponseResult) o; + ListItemsListResponseResult listItemsListResponseResult = (ListItemsListResponseResult) o; return Objects.equals(this.data, listItemsListResponseResult.data); } From 4452596eab374c80504ef850b5c1ede3ec77e43e Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Fri, 16 Oct 2026 14:55:17 -0400 Subject: [PATCH 08/15] Force type deserialization on client, changes to support this - Addtional changes to processGermplasmForDisplay that was not occuring before. This was breaking features in the Lists tab --- .../io/swagger/model/core/ListItemsListResponse.java | 11 +++++++++++ .../controller/core/ListsApiController.java | 9 +++++++-- .../BrAPITestServer/service/core/ListItemService.java | 4 +--- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/swagger/model/core/ListItemsListResponse.java b/src/main/java/io/swagger/model/core/ListItemsListResponse.java index a5021c38..eb9c80d0 100644 --- a/src/main/java/io/swagger/model/core/ListItemsListResponse.java +++ b/src/main/java/io/swagger/model/core/ListItemsListResponse.java @@ -22,6 +22,9 @@ public class ListItemsListResponse implements BrAPIResponse result = null; @@ -78,6 +81,14 @@ public void setResult(ListItemsListResponseResult result) { this.result = result; } + public ListTypes getResultListType() { + return resultListType; + } + + public void setResultListType(ListTypes resultListType) { + this.resultListType = resultListType; + } + @Override public boolean equals(java.lang.Object o) { if (this == o) { diff --git a/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java b/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java index 4c8fd78d..a6d9fd54 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/controller/core/ListsApiController.java @@ -221,8 +221,13 @@ public ResponseEntity searchListByItemRequest( validateAcceptHeader(request); Metadata metadata = generateMetaDataTemplate(body); - List> data = listItemService.search(listDbId, body, metadata); - return responseOK(new ListItemsListResponse(), new ListItemsListResponseResult(), data, metadata); + ListDetails list = listService.getList(listDbId); + List> data = listItemService.search(list, body, metadata); + + ListItemsListResponse listItemsListResponse = new ListItemsListResponse(); + listItemsListResponse.setResultListType(list.getListType()); + + return responseOK(listItemsListResponse, new ListItemsListResponseResult(), data, metadata); } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java index 6c46d4c0..316e60bc 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java @@ -34,11 +34,9 @@ public ListItemService(ListService listService, )); } - public List> search(String listDbId, + public List> search(ListDetails list, JsonNode searchRequest, Metadata metadata) throws BrAPIServerException { - ListDetails list = listService.getList(listDbId); - ListItemSearchRequestHandler handler = handlers.get(list.getListType()); if (handler == null) { From 500a83393a825ffce4c275f27e2a402d74f7d513 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Fri, 16 Oct 2026 15:21:04 -0400 Subject: [PATCH 09/15] Validate fields in submitted search request to ensure proper searches --- .../service/core/ListItemService.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java index 316e60bc..7e5df237 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java @@ -1,7 +1,10 @@ package org.brapi.test.BrAPITestServer.service.core; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; import io.swagger.model.Metadata; import io.swagger.model.core.*; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; @@ -11,8 +14,10 @@ import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.stereotype.Service; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -53,6 +58,8 @@ private List> executeHandler(ListItemSearchRequestHa T typedSearchRequest; + validateSearchRequest(list, request, handler); + try { typedSearchRequest = objectMapper.convertValue( request, @@ -69,4 +76,36 @@ private List> executeHandler(ListItemSearchRequestHa return handler.search(list, typedSearchRequest, metadata); } + + private void validateSearchRequest(ListDetails list, JsonNode request, ListItemSearchRequestHandler handler) throws BrAPIServerException { + JavaType javaType = objectMapper.getTypeFactory() + .constructType(handler.getSearchRequestClass()); + + BeanDescription beanDescription = objectMapper.getDeserializationConfig() + .introspect(javaType); + + Set allowedProperties = beanDescription.findProperties() + .stream() + .map(BeanPropertyDefinition::getName) + .collect(Collectors.toSet()); + + Iterator fields = request.fieldNames(); + + while (fields.hasNext()) { + String field = fields.next(); + + if (!allowedProperties.contains(field)) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, + String.format("List with dbId [%s] is of type [%s]. The expected search request to be submitted to this endpoint is a " + + "[%s], but a search request property [%s] was found that does not exist in the allowed search request for this list type. " + + "Please ensure any fields being searched match the schema of a [%s] exactly before submitting to ensure correct data is retrieved.", + list.getListDbId(), + list.getListType(), + handler.getSearchRequestClass().getSuperclass().getSimpleName(), + field, + handler.getSearchRequestClass().getSuperclass().getSimpleName()) + ); + } + } + } } From 6bb6f83bf0200885e2eedcdc6b7e781c9564e9d4 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 19 Oct 2026 13:54:29 -0400 Subject: [PATCH 10/15] Simplify validation logic --- .../service/core/ListItemService.java | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java index 7e5df237..59cfe885 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java @@ -1,9 +1,8 @@ package org.brapi.test.BrAPITestServer.service.core; -import com.fasterxml.jackson.databind.BeanDescription; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; import io.swagger.model.Metadata; import io.swagger.model.core.*; @@ -14,6 +13,7 @@ import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.stereotype.Service; +import java.io.IOException; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -78,23 +78,15 @@ private List> executeHandler(ListItemSearchRequestHa } private void validateSearchRequest(ListDetails list, JsonNode request, ListItemSearchRequestHandler handler) throws BrAPIServerException { - JavaType javaType = objectMapper.getTypeFactory() - .constructType(handler.getSearchRequestClass()); - BeanDescription beanDescription = objectMapper.getDeserializationConfig() - .introspect(javaType); + ObjectReader reader = objectMapper + .readerFor(handler.getSearchRequestClass()) + .with(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - Set allowedProperties = beanDescription.findProperties() - .stream() - .map(BeanPropertyDefinition::getName) - .collect(Collectors.toSet()); - Iterator fields = request.fieldNames(); - - while (fields.hasNext()) { - String field = fields.next(); - - if (!allowedProperties.contains(field)) { + try (JsonParser parser = request.traverse(objectMapper)) { + reader.readValue(parser); + } catch (UnrecognizedPropertyException e) { throw new BrAPIServerException(HttpStatus.BAD_REQUEST, String.format("List with dbId [%s] is of type [%s]. The expected search request to be submitted to this endpoint is a " + "[%s], but a search request property [%s] was found that does not exist in the allowed search request for this list type. " + @@ -102,10 +94,19 @@ private void validateSearchRequest(ListDetails list, JsonNode request, ListI list.getListDbId(), list.getListType(), handler.getSearchRequestClass().getSuperclass().getSimpleName(), - field, + e.getPropertyName(), handler.getSearchRequestClass().getSuperclass().getSimpleName()) ); - } + } catch (IOException e) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, + String.format("List with dbId [%s] is of type [%s]. The expected search request to be submitted to this endpoint is a " + + "[%s], but a part of the request is malformed so that it is incompatible with the targeted request type." + + "Please ensure any fields being searched match the schema of a [%s] exactly before submitting to ensure correct data is retrieved.", + list.getListDbId(), + list.getListType(), + handler.getSearchRequestClass().getSuperclass().getSimpleName(), + handler.getSearchRequestClass().getSuperclass().getSimpleName()) + ); } } } From 4a31b8e8d8e4147a4f4947f70dc36f484bf43922 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 19 Oct 2026 15:35:07 -0400 Subject: [PATCH 11/15] Add clarifying comments --- .../model/core/GermplasmListSearchRequest.java | 2 +- .../swagger/model/core/ListItemWithFullItem.java | 1 + .../swagger/model/core/ListItemsListResponse.java | 1 + .../model/germ/GermplasmSearchRequest.java | 2 +- .../controller/core/BrAPIController.java | 1 + .../controller/core/ListsApiController.java | 1 + .../handlers/ListItemSearchRequestHandler.java | 10 ++++++++++ .../primaryEntities/BrAPIRepositoryImpl.java | 15 +++++++++++++++ .../service/core/ListItemService.java | 10 ++++++---- .../service/germ/GermplasmService.java | 15 ++++++++++++++- 10 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java b/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java index 2ece7db8..5768d82b 100644 --- a/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java +++ b/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java @@ -10,7 +10,7 @@ public class GermplasmListSearchRequest extends GermplasmSearchRequest { // The only real difference in this request is that there's an extra sort/filter field that GermplasmSearchRequest doesn't have. - // It is specific to lists. + // It is specific to lists. In general, other EntityListSearchRequests like this one should look the same, but have different base search requests. private static final Map LIST_ALLOWED_FIELDS; diff --git a/src/main/java/io/swagger/model/core/ListItemWithFullItem.java b/src/main/java/io/swagger/model/core/ListItemWithFullItem.java index f091c0a1..abe035c0 100644 --- a/src/main/java/io/swagger/model/core/ListItemWithFullItem.java +++ b/src/main/java/io/swagger/model/core/ListItemWithFullItem.java @@ -14,6 +14,7 @@ public class ListItemWithFullItem { @JsonProperty("listItem") private ListItem listItem = null; + // The full actual BrAPI model object of the list item is supplied/written into here @JsonProperty("item") private T item = null; diff --git a/src/main/java/io/swagger/model/core/ListItemsListResponse.java b/src/main/java/io/swagger/model/core/ListItemsListResponse.java index eb9c80d0..7b70e536 100644 --- a/src/main/java/io/swagger/model/core/ListItemsListResponse.java +++ b/src/main/java/io/swagger/model/core/ListItemsListResponse.java @@ -22,6 +22,7 @@ public class ListItemsListResponse implements BrAPIResponse searchListByItemRequest( ListDetails list = listService.getList(listDbId); List> data = listItemService.search(list, body, metadata); + // Setting the resultListType here allows the BrAPI client to know how to initialize the parameterized ListItemWithFullItem.item class ListItemsListResponse listItemsListResponse = new ListItemsListResponse(); listItemsListResponse.setResultListType(list.getListType()); diff --git a/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java b/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java index 12e988ed..29e3481f 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java @@ -8,6 +8,16 @@ import java.util.List; +// TODO: Implement for every available list type. For now, Germplasm is the only implementation required by DeltaBreed. +/** + * This is a generic interface for any list item search for any list type to implement. + * + * This interface is designed to have implementors pass a EntityListSearchRequest, like GermplasmListSearchRequest, as the parameterized object. + * The implementations must then provide the intended ListTypes enum value for this request, the EntityListSearchRequest class object, + * and a dedicated search method to carry out the search. + * + * See ListsApiController.searchListByItemRequest(), ListItemService.search() for usages, and GermplasmListSearchRequestHandler for an implementation. + */ public interface ListItemSearchRequestHandler { ListTypes getListType(); diff --git a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java index 61a01633..b0977e7c 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/repository/primaryEntities/BrAPIRepositoryImpl.java @@ -60,6 +60,21 @@ public Page findAllBySearchAndPaginate(SearchQueryBuilder searchQuery, Pag return new PageImpl<>(content, pageReq, totalCount); } + /** + * Supports paginated queries that select on multiple entities, like this: + * + * SELECT listItem, entity FROM GermplasmEntity entity + * JOIN ListItemEntity listItem ON listItem.item = entity.germplasmName + * + * The resulting Page represents each entity as an element in the tuple from the select, so in the example above, + * tuple.get(0) = ListItemEntity + * tuple.get(1) = GermplasmEntity + * + * This is helpful when there are requirements to sort/filter/paginate an entity in relation to another object, which is the exact + * use case for ListsApiController.searchListByItemRequest(). + * + * For example usages of this method, see GermplasmService.findGermplasmByList() + */ public Page findAllBySearchAndPaginateMultipleEntitySelect(SearchQueryBuilder searchQuery, Pageable pageReq) throws InvalidPagingException { applyUserId(searchQuery); diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java index 59cfe885..1c846e07 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/ListItemService.java @@ -3,21 +3,17 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; -import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; import io.swagger.model.Metadata; import io.swagger.model.core.*; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; import org.brapi.test.BrAPITestServer.handlers.ListItemSearchRequestHandler; -import org.brapi.test.BrAPITestServer.model.entity.core.ListItemEntity; import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.stereotype.Service; import java.io.IOException; -import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -32,6 +28,8 @@ public ListItemService(ListService listService, ObjectMapper objectMapper) { this.listService = listService; this.objectMapper = objectMapper; + // Initializes all ListItemSearchRequestHandlers by enumerating through the available BrAPI ListTypes. + // Right now, GermplasmListSearchRequestHandler is the only implementation available. this.handlers = handlerList.stream() .collect(Collectors.toMap( ListItemSearchRequestHandler::getListType, @@ -45,6 +43,7 @@ public List> search(ListDetails list, ListItemSearchRequestHandler handler = handlers.get(list.getListType()); if (handler == null) { + // List Type does not have a handler yet. Implement ListItemSearchRequestHandler for the BrAPI ListType, using GermplasmListSearchRequestHandler as an example. throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "Unsupported list type: " + list.getListType()); } @@ -77,6 +76,9 @@ private List> executeHandler(ListItemSearchRequestHa return handler.search(list, typedSearchRequest, metadata); } + /** + * Ensures submitted request contains fields/properties only relevant to the BrAPI search request for the list type. + */ private void validateSearchRequest(ListDetails list, JsonNode request, ListItemSearchRequestHandler handler) throws BrAPIServerException { ObjectReader reader = objectMapper diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java index 0f786abb..a6d0c03c 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java @@ -491,6 +491,9 @@ private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder> findGermplasmByList(ListDetails list, GermplasmListSearchRequest request, Metadata metadata) throws BrAPIServerException{ Page page = searchGermplasmByList(list, request, metadata); @@ -1185,10 +1188,11 @@ public Page searchGermplasmByList(ListDetails list, GermplasmListSearchRe if (sortByElements == null || sortByElements.isEmpty()) { // If there is no specified sort, sort by the position of the list. - SortBy defaultListItemSort = new SortBy("importEntryNumber", SortOrder.ASC); + SortBy defaultListItemSort = new SortBy("listItemPosition", SortOrder.ASC); searchRequest.setSortBy(List.of(defaultListItemSort)); } + // This generates the search query which has any filters, sorting, and where's derived from the incoming search request. SearchQueryBuilder searchQuery = buildGermplasmSearchQuery(searchRequest); searchQuery.modifySelect(List.of("listItem", "entity")); @@ -1198,6 +1202,15 @@ public Page searchGermplasmByList(ListDetails list, GermplasmListSearchRe .appendSingle(UUID.fromString(list.getListDbId()), "*list.id") .appendEnum(list.getListType(), "*list.listType"); + // At this point the search request should look something like: + // SELECT listItem, entity FROM GermplasmEntity entity + // JOIN ListItemEntity listItem ON entity.germplasmName = listItem.item + // JOIN listItem.list list + // (any other joins appearing came from buildGermplasmSearchQuery criteria) + // WHERE list.id IN (?) AND list.listType = ? AND (any other filters appearing came from buildGermplasmSearchQuery criteria) + // SORT BY (whatever is defined for listItemPosition in GermplasmListSearchRequest) ASC + // (If any other sorts appear here they were provided in the search request and were generated by buildGermplasmSearchQuery) + Page listItemAndGerm = germplasmRepository.findAllBySearchAndPaginateMultipleEntitySelect(searchQuery, pageReq); Page germs = listItemAndGerm.map( From 677d759c377f13acb2d85901426be0950a2426a6 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 19 Oct 2026 16:56:53 -0400 Subject: [PATCH 12/15] Fix alias bug with synonyms --- .../test/BrAPITestServer/service/SearchQueryBuilder.java | 2 +- .../BrAPITestServer/service/germ/GermplasmService.java | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index bcd411ec..dbbdf04a 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -536,7 +536,7 @@ public SearchQueryBuilder filterBy(List filterBy, Map findGermplasmEntities(@Valid GermplasmSearchRequest // Since the built searchQuery contains a lazily loaded collection retrieved with a LEFT JOIN FETCH, // use findAllBySearchPaginatingWithFetches() + // TODO: As of BI-3082, the lazily loaded collection synonyms has been removed the initial search query. + // TODO: Because of this, we should be able to change this call to findAllBySearchAndPaginate, but this should be tracked as a separate effort. [BI-3084] Page germs = germplasmRepository.findAllBySearchPaginatingWithFetches(searchQuery, pageReq); // Hopefully this retains insertion order in the page? @@ -196,7 +198,7 @@ private SearchQueryBuilder buildGermplasmSearchQuery(GermplasmS .appendList(request.getStudyNames(), "*obsunit.study.studyName"); } if (request.getSynonyms() != null) { - searchQuery = searchQuery.join("synonyms", "synonym").appendList(request.getSynonyms(), "*synonym.synonym"); + searchQuery = searchQuery.join("synonyms", "synonyms").appendList(request.getSynonyms(), "*synonyms.synonym"); } if (request.getInstituteCodes() != null) { searchQuery = searchQuery.join("institutes", "institute").appendList(request.getSynonyms(), @@ -368,7 +370,7 @@ private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder synonyms = germplasmRepository.findAllBySearch(searchQuery); @@ -383,6 +385,7 @@ private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder xrefs = germplasmRepository.findAllBySearch(searchQuery); From aa58f0610c480b9721f2e2299ca203c2e05a89c9 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 19 Oct 2026 17:00:09 -0400 Subject: [PATCH 13/15] Add comment to BrAPIRepositoryImpl --- .../brapi/test/BrAPITestServer/service/SearchQueryBuilder.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index dbbdf04a..62a50ad3 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -435,6 +435,8 @@ private String generateLeftJoinFetch(String join, String paramName) { return "LEFT JOIN FETCH " + entityPrefix(join) + " " + paramFilter(paramName) + " "; } + // Here * is used as a trigger to use the subsequent field string after it directly on a hibernate query instead of + // entity.field. This is important in many query builds, including filter/sort queries private String entityPrefix(String field) { if (field.startsWith("*")) { return field.substring(1); From 84ec9bd94e6b70cf475c9495c4c2afcb3d7c5bc6 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 19 Oct 2026 17:05:03 -0400 Subject: [PATCH 14/15] Add comments to SortAndFilterEntityColumnInfo --- .../model/dto/SortAndFilterEntityColumnInfo.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/brapi/test/BrAPITestServer/model/dto/SortAndFilterEntityColumnInfo.java b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/SortAndFilterEntityColumnInfo.java index 8fbf43e1..60cd42ed 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/model/dto/SortAndFilterEntityColumnInfo.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/SortAndFilterEntityColumnInfo.java @@ -7,7 +7,12 @@ public class SortAndFilterEntityColumnInfo { private final String entityColumnName; private final EntityType entityType; + // This boolean tells us effectively if the column is sortable. Hibernate does not support sorting on collections, as it is confusing. + // If this feature is desired, a new subquery would likely have to be created to achieve this on the collection. + // See SearchQueryBuilder.sortBy() for usage. private final boolean isCollectionOnEntity; + // This boolean tells us if the column has been joined on a table that does not exist in the target entity. + // In this case it's likely we want different join logic, see SearchQueryBuilder.sortBy or .filterBy for usages private final boolean isJoinOnUnrelatedEntity; From 1fc0445c80607f5b8b55eb26e6f5a69f475468d0 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 19 Oct 2026 17:08:13 -0400 Subject: [PATCH 15/15] Add comment for extra synonyms join --- .../test/BrAPITestServer/service/germ/GermplasmService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java index 25019e79..a6c6dbbf 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java @@ -385,6 +385,8 @@ private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder xrefs = germplasmRepository.findAllBySearch(searchQuery);