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 extends BrAPIResponse> 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 extends BrAPIResponse> 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 extends BrAPIResponse> searchListsSearchResultsDbIdGet(
}
}
+ @CrossOrigin
+ @Override
+ public ResponseEntity extends BrAPIResponse> 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;
+ }
+
}