diff --git a/src/main/java/io/swagger/api/core/ListsApi.java b/src/main/java/io/swagger/api/core/ListsApi.java index b4c95e98..8d346d29 100644 --- a/src/main/java/io/swagger/api/core/ListsApi.java +++ b/src/main/java/io/swagger/api/core/ListsApi.java @@ -5,13 +5,10 @@ */ 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.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 +163,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 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/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 new file mode 100644 index 00000000..5768d82b --- /dev/null +++ b/src/main/java/io/swagger/model/core/GermplasmListSearchRequest.java @@ -0,0 +1,33 @@ +package io.swagger.model.core; + +import io.swagger.model.germ.GermplasmSearchRequest; +import org.brapi.test.BrAPITestServer.model.dto.SortAndFilterEntityColumnInfo; +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. In general, other EntityListSearchRequests like this one should look the same, but have different base search requests. + private static final Map + LIST_ALLOWED_FIELDS; + + static { + Map fields = + copyAllowedSortAndFilterFields(); + + fields.put( + "listItemPosition", + new SortAndFilterEntityColumnInfo("*listItem.positionIndexedBy1", EntityType.NUMBER, false, true) + ); + + LIST_ALLOWED_FIELDS = Collections.unmodifiableMap(fields); + } + + @Override + 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 new file mode 100644 index 00000000..a3f70dd2 --- /dev/null +++ b/src/main/java/io/swagger/model/core/ListItem.java @@ -0,0 +1,95 @@ +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; + + /** + * 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/ListItemWithFullItem.java b/src/main/java/io/swagger/model/core/ListItemWithFullItem.java new file mode 100644 index 00000000..abe035c0 --- /dev/null +++ b/src/main/java/io/swagger/model/core/ListItemWithFullItem.java @@ -0,0 +1,83 @@ +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; + + // The full actual BrAPI model object of the list item is supplied/written into here + @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..7b70e536 --- /dev/null +++ b/src/main/java/io/swagger/model/core/ListItemsListResponse.java @@ -0,0 +1,134 @@ +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; + + // This piece of data is used by the brapi client to determine and populate the exact data model return type of ListItemWithFullItem.item + @JsonProperty("resultListType") + private ListTypes resultListType = 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; + } + + public ListTypes getResultListType() { + return resultListType; + } + + public void setResultListType(ListTypes resultListType) { + this.resultListType = resultListType; + } + + @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..892a2dba --- /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/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 ed030625..f25a5052 100644 --- a/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java +++ b/src/main/java/io/swagger/model/germ/GermplasmSearchRequest.java @@ -3,34 +3,35 @@ 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.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; 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("importEntryNumber", EntityType.NUMBER, false, true)), + Map.entry("synonyms", new SortAndFilterEntityColumnInfo("*synonyms.synonym", EntityType.TEXT, true, false)) ); + protected static Map copyAllowedSortAndFilterFields() { + return new HashMap<>(ALLOWED_SORT_AND_FILTER_FIELDS); + } + @JsonProperty("accessionNumbers") private List accessionNumbers = null; @@ -641,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/controller/core/BrAPIController.java b/src/main/java/org/brapi/test/BrAPITestServer/controller/core/BrAPIController.java index 05cd18bf..90f13064 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,26 @@ protected Metadata generateMetaDataTemplate(String pageToken, Integer pageSize) return metaData; } + // Used when endpoints require generic, untyped search bodies. (See ListsApiController.searchListByItemRequest) + 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 ce653cdf..43e831bd 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.*; @@ -12,6 +13,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 +41,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 +209,26 @@ public ResponseEntity searchListsSearchResultsDbIdGet( } } + @CrossOrigin + @Override + public ResponseEntity searchListByItemRequest( + @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 = generateMetaDataTemplate(body); + + 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()); + + return responseOK(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..b1d731b7 --- /dev/null +++ b/src/main/java/org/brapi/test/BrAPITestServer/handlers/GermplasmListSearchRequestHandler.java @@ -0,0 +1,37 @@ +package org.brapi.test.BrAPITestServer.handlers; + +import io.swagger.model.Metadata; +import io.swagger.model.core.*; +import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; +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, + Metadata metadata) throws BrAPIServerException { + + return germplasmService.findGermplasmByList(list, searchRequest, metadata); + } +} 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..29e3481f --- /dev/null +++ b/src/main/java/org/brapi/test/BrAPITestServer/handlers/ListItemSearchRequestHandler.java @@ -0,0 +1,31 @@ +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.ListTypes; +import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; + +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(); + + Class getSearchRequestClass(); + + List> search( + ListDetails list, + T searchRequest, + Metadata metadata + ) throws BrAPIServerException; +} 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..60cd42ed --- /dev/null +++ b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/SortAndFilterEntityColumnInfo.java @@ -0,0 +1,53 @@ +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; + // 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; + + + 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/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; + } } 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..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; @@ -27,10 +28,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); + + 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 364cfd8d..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 @@ -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,38 @@ 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); + + Long totalCount = getMultipleSelectCountQuery(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. @@ -73,6 +106,7 @@ public Page findAllBySearchAndPaginate(SearchQueryBuilder searchQuery, Pag * 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 { @@ -181,6 +215,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 +250,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); @@ -220,6 +271,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 855aaf4c..62a50ad3 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -10,7 +10,7 @@ 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; @@ -19,6 +19,8 @@ public class SearchQueryBuilder { private String selectClause; private String selectOnlyIds; private String selectOnlyIdsSubquery; + private String joinClause; + private String joinClauseIdSubquery; private String whereClause; private String defaultSort; private String sortClause; @@ -28,17 +30,19 @@ public class SearchQueryBuilder { private Class clazz; private final String default_date = "1970-01-01"; - 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() { @@ -47,7 +51,23 @@ public String getQuery() { sortClause = defaultSort; } - return selectClause + whereClause + sortClause; + 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; } /** @@ -86,7 +106,7 @@ public String getIdQuery() { String entityRegex = "(? 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()) { @@ -338,15 +365,24 @@ public SearchQueryBuilder 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; } + 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); } @@ -356,11 +392,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; @@ -375,8 +411,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); @@ -389,8 +425,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; } @@ -399,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); @@ -428,19 +466,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"); } @@ -451,7 +489,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()) { @@ -497,21 +536,22 @@ public SearchQueryBuilder filterBy(List filterBy, Map> handlers; + private final ObjectMapper objectMapper; + + public ListItemService(ListService listService, + List> handlerList, + 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, + Function.identity() + )); + } + + public List> search(ListDetails list, + JsonNode searchRequest, + Metadata metadata) throws BrAPIServerException { + 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()); + } + + return executeHandler(handler, list, searchRequest, metadata); + } + + private List> executeHandler(ListItemSearchRequestHandler handler, + ListDetails list, + JsonNode request, + Metadata metadata) throws BrAPIServerException { + + T typedSearchRequest; + + validateSearchRequest(list, request, handler); + + 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); + } + + /** + * 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 + .readerFor(handler.getSearchRequestClass()) + .with(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + + + 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. " + + "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(), + 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()) + ); + } + } +} diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java index 096e9cf7..3995d4f2 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java @@ -159,8 +159,8 @@ public List findStudies(StudySearchRequest request, Metadata metaData) .appendList(request.getStudyDbIds(), "id").appendList(request.getStudyNames(), "studyName") .appendList(request.getStudyPUIs(), "studyPUI").appendList(request.getStudyTypes(), "studyType") .appendList(request.getTrialDbIds(), "trial.id").appendList(request.getTrialNames(), "trial.trialName") - .sortBy(request.getSortByElements(), request.getEntityColAndTypeBySubmittedNameMap()) - .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 36485c6d..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 @@ -4,8 +4,14 @@ import java.util.*; import java.util.stream.Collectors; -import io.swagger.model.core.ProgramSearchRequest; +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; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerDbIdNotFoundException; @@ -13,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; @@ -30,7 +37,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; @@ -130,6 +136,8 @@ public Page 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? @@ -138,6 +146,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"); @@ -173,8 +183,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"); @@ -189,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(), @@ -208,8 +217,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; } @@ -218,6 +227,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); @@ -337,16 +361,33 @@ 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", true); + + 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"); + // Because the join was overwritten above, the original synonym join was removed with the replace. This extra join restores it so + // queries with join in the where clause can continue to work. + searchQuery.join("synonyms", "synonyms"); List xrefs = germplasmRepository.findAllBySearch(searchQuery); @@ -455,6 +496,19 @@ private void fetchRemainingGermCollectionsUsingQuery(SearchQueryBuilder> findGermplasmByList(ListDetails list, GermplasmListSearchRequest request, Metadata metadata) throws BrAPIServerException{ + + Page page = searchGermplasmByList(list, request, metadata); + + Page> listItems = page.map(this::convertFromEntity); + PagingUtility.calculateMetaData(metadata, page); + + return listItems.getContent(); + } + public Germplasm getGermplasm(String germplasmDbId) throws BrAPIServerException { return convertFromEntity(getGermplasmEntity(germplasmDbId, HttpStatus.NOT_FOUND)); } @@ -520,7 +574,24 @@ public List saveGermplasm(@Valid List body) thro .collect(Collectors.toList()); } - private Germplasm convertFromEntity(GermplasmEntity entity) { + 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); @@ -1114,4 +1185,70 @@ private List getRedundantInstitutes( return dupInstitutes; } + 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("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")); + + 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"); + + // 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( + // 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; + } + }