diff --git a/pom.xml b/pom.xml index 962d101a..a1e201c5 100644 --- a/pom.xml +++ b/pom.xml @@ -20,6 +20,7 @@ 21 2.10.0 2.0.16 + 2.18.1 @@ -73,28 +74,11 @@ io.jsonwebtoken jjwt 0.9.1 - - - com.fasterxml.jackson.core - jackson-databind - - com.google.api-client google-api-client 1.27.0 - - - com.fasterxml.jackson.core - jackson-core - - - - - com.fasterxml.jackson.core - jackson-databind - [2.9.9.1,) io.springfox @@ -106,11 +90,6 @@ springfox-swagger-ui ${springfox-version} - - com.github.joschi.jackson - jackson-datatype-threetenbp - 2.8.4 - jakarta.annotation jakarta.annotation-api @@ -143,6 +122,18 @@ + + + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + + + + @@ -161,7 +152,7 @@ org.springframework.boot spring-boot-maven-plugin - 2.7.5 + ${spring.version} @@ -173,21 +164,4 @@ - - - spring-releases - https://repo.spring.io/libs-release - - - spring-milestone - https://repo.spring.io/libs-milestone - - - - - spring-releases - https://repo.spring.io/libs-release - - - diff --git a/src/main/java/io/swagger/model/FilterBy.java b/src/main/java/io/swagger/model/FilterBy.java new file mode 100644 index 00000000..acbc03e7 --- /dev/null +++ b/src/main/java/io/swagger/model/FilterBy.java @@ -0,0 +1,24 @@ +package io.swagger.model; + +public class FilterBy { + + private String filterOn; + private String value; + + public String getFilterOn() { + return filterOn; + } + + public void setFilterOn(String filterOn) { + this.filterOn = filterOn; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + +} diff --git a/src/main/java/io/swagger/model/SearchRequest.java b/src/main/java/io/swagger/model/SearchRequest.java index 8ad217c4..aaad3b9a 100644 --- a/src/main/java/io/swagger/model/SearchRequest.java +++ b/src/main/java/io/swagger/model/SearchRequest.java @@ -2,9 +2,15 @@ import com.fasterxml.jackson.annotation.JsonIgnore; 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.EntityType; +import org.springframework.http.HttpStatus; import java.util.ArrayList; import java.util.List; +import java.util.Map; public abstract class SearchRequest { @JsonIgnore @@ -25,6 +31,20 @@ public abstract class SearchRequest { @JsonProperty("externalReferenceSources") protected List externalReferenceSources = null; + @JsonProperty("filterBy") + protected List filterBy = null; + + @JsonProperty("sortBy") + protected List sortBy = null; + + @JsonIgnore + protected Map sortFilterEntityColumnNamesByRequestName = null; + + @JsonIgnore + public List getExternalReferenceIds() { + return externalReferenceIds; + } + final public SearchRequest page(Integer page) { this.page = page; return this; @@ -119,4 +139,75 @@ public void addExternalReferenceItem(String externalReferenceId, String external } } + + public List getFilterBy() { + return filterBy; + } + + public void setFilterBy(List filterBy) throws BrAPIServerException { + + if (filterBy == null || filterBy.isEmpty()) { + return; + } + + Map allowedSortFilterNames = getEntityColAndTypeBySubmittedNameMap(); + + for (FilterBy filterByItem : filterBy) { + + if (filterByItem.getFilterOn() == null || filterByItem.getFilterOn().isEmpty()) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "filterOn attribute not provided in element of filterBy list."); + } + + if (filterByItem.getValue() == null || filterByItem.getValue().isEmpty()) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "value attribute not provided in element of filterBy list."); + } + + EntityColumnNameAndType entityColumnNameAndType = allowedSortFilterNames.get(filterByItem.getFilterOn()); + + if (entityColumnNameAndType == 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) { + // TODO: Add support for this when it becomes relevant for BI or when there is time. + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, String.format("Filtering not implemented for column name [%s] with associated data type [%s]", entityColumnNameAndType.getEntityColumnName(), EntityType.BOOLEAN)); + } + } + this.filterBy = filterBy; + } + + public List getSortByElements() { + return sortBy; + } + + public void setSortBy(List sortBy) throws BrAPIServerException { + + if (sortBy == null || sortBy.isEmpty()) { + return; + } + + Map allowedSortFilterNames = getEntityColAndTypeBySubmittedNameMap(); + + for (SortBy sortByItem : sortBy) { + if (sortByItem.getSortedOn() == null || sortByItem.getSortedOn().isEmpty()) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "sortedOn attribute not provided in element of sortBy list"); + } + + EntityColumnNameAndType sortColumnEntityNameAndType = getEntityColAndTypeBySubmittedNameMap().get(sortByItem.getSortedOn()); + + if (sortColumnEntityNameAndType == null) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, + String.format("Supplied sortColumn [%s] not available in allowed names [%s]", sortByItem.getSortedOn(), allowedSortFilterNames.keySet()) + ); + } + } + + this.sortBy = sortBy; + } + + public Map getEntityColAndTypeBySubmittedNameMap() throws BrAPIServerException { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, String.format("Sort/Filtering not implemented for %s", this.getClass().getSimpleName())); + } } diff --git a/src/main/java/io/swagger/model/core/SortBy.java b/src/main/java/io/swagger/model/core/SortBy.java deleted file mode 100644 index 8a132a3e..00000000 --- a/src/main/java/io/swagger/model/core/SortBy.java +++ /dev/null @@ -1,56 +0,0 @@ -package io.swagger.model.core; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - -public enum SortBy { - - STUDYDBID("studyDbId"), - - STARTDATE("startDate"), - - ENDDATE("endDate"), - - TRIALDBID("trialDbId"), - - TRIALNAME("trialName"), - - PROGRAMDBID("programDbId"), - - LOCATIONDBID("locationDbId"), - - SEASONDBID("seasonDbId"), - - STUDYTYPE("studyType"), - - STUDYNAME("studyName"), - - STUDYLOCATION("studyLocation"), - - PROGRAMNAME("programName"), - - GERMPLASMDBID("germplasmDbId"), - - OBSERVATIONVARIABLEDBID("observationVariableDbId"); -private String value; - - SortBy(String value) { - this.value = value; - } - - @Override - @JsonValue - public String toString() { - return String.valueOf(value); - } - - @JsonCreator - public static SortBy fromValue(String text) { - for (SortBy b : SortBy.values()) { - if (String.valueOf(b.value).equals(text)) { - return b; - } - } - return null; - } -} diff --git a/src/main/java/io/swagger/model/core/StudySearchRequest.java b/src/main/java/io/swagger/model/core/StudySearchRequest.java index 0b26436b..94e760ff 100644 --- a/src/main/java/io/swagger/model/core/StudySearchRequest.java +++ b/src/main/java/io/swagger/model/core/StudySearchRequest.java @@ -1,14 +1,36 @@ package io.swagger.model.core; +import java.util.Map; import java.util.Objects; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.model.SearchRequest; +import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; +import org.brapi.test.BrAPITestServer.model.dto.EntityType; + import java.util.ArrayList; import java.util.List; public class StudySearchRequest extends SearchRequest { + + // Key - allowed sort or field filter name for this entity + // Value = entity field name and type that represents the submitted field. Used later on in query building. + private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = + Map.ofEntries( + Map.entry("germplasmDbId", new EntityColumnNameAndType("*obsunit.germplasm.id", EntityType.UUID)), + Map.entry("locationDbId", new EntityColumnNameAndType("location.id", EntityType.UUID)), + Map.entry("observationVariableDbId", new EntityColumnNameAndType("*observation.observationVariable.id", EntityType.UUID)), + Map.entry("programDbId", new EntityColumnNameAndType("trial.program.id", EntityType.UUID)), + Map.entry("programName", new EntityColumnNameAndType("trial.program.name", EntityType.TEXT)), + Map.entry("seasonDbId", new EntityColumnNameAndType("*season.id", EntityType.UUID)), + Map.entry("studyDbId", new EntityColumnNameAndType("id", EntityType.UUID)), + Map.entry("studyLocation", new EntityColumnNameAndType("location.id", EntityType.UUID)), + Map.entry("trialDbId", new EntityColumnNameAndType("trial.id", EntityType.UUID)), + Map.entry("studyType", new EntityColumnNameAndType("studyName", EntityType.TEXT)), + Map.entry("studyName", new EntityColumnNameAndType("studyName", EntityType.TEXT)) + ); + @JsonProperty("commonCropNames") private List commonCropNames = null; @@ -54,12 +76,6 @@ public class StudySearchRequest extends SearchRequest { @JsonProperty("seasonDbIds") private List seasonDbIds = null; - @JsonProperty("sortBy") - private SortBy sortBy = null; - - @JsonProperty("sortOrder") - private SortOrder sortOrder = null; - @JsonProperty("studyCodes") private List studyCodes = null; @@ -376,32 +392,6 @@ public void setSeasonDbIds(List seasonDbIds) { this.seasonDbIds = seasonDbIds; } - public StudySearchRequest sortBy(SortBy sortBy) { - this.sortBy = sortBy; - return this; - } - - public SortBy getSortBy() { - return sortBy; - } - - public void setSortBy(SortBy sortBy) { - this.sortBy = sortBy; - } - - public StudySearchRequest sortOrder(SortOrder sortOrder) { - this.sortOrder = sortOrder; - return this; - } - - public SortOrder getSortOrder() { - return sortOrder; - } - - public void setSortOrder(SortOrder sortOrder) { - this.sortOrder = sortOrder; - } - public StudySearchRequest studyCodes(List studyCodes) { this.studyCodes = studyCodes; return this; @@ -492,7 +482,6 @@ public boolean equals(java.lang.Object o) { && Objects.equals(this.active, studySearchRequest.active) && Objects.equals(this.seasonDbIds, studySearchRequest.seasonDbIds) && Objects.equals(this.sortBy, studySearchRequest.sortBy) - && Objects.equals(this.sortOrder, studySearchRequest.sortOrder) && Objects.equals(this.studyCodes, studySearchRequest.studyCodes) && Objects.equals(this.studyPUIs, studySearchRequest.studyPUIs) && Objects.equals(this.studyTypes, studySearchRequest.studyTypes) && super.equals(o); @@ -503,7 +492,7 @@ public int hashCode() { return Objects.hash(commonCropNames, programDbIds, programNames, trialDbIds, trialNames, studyDbIds, studyNames, locationDbIds, locationNames, germplasmDbIds, germplasmNames, observationVariableDbIds, observationVariableNames, externalReferenceIds, externalReferenceSources, active, seasonDbIds, sortBy, - sortOrder, studyCodes, studyPUIs, studyTypes, super.hashCode()); + studyCodes, studyPUIs, studyTypes, super.hashCode()); } @Override @@ -529,7 +518,6 @@ public String toString() { sb.append(" active: ").append(toIndentedString(active)).append("\n"); sb.append(" seasonDbIds: ").append(toIndentedString(seasonDbIds)).append("\n"); sb.append(" sortBy: ").append(toIndentedString(sortBy)).append("\n"); - sb.append(" sortOrder: ").append(toIndentedString(sortOrder)).append("\n"); sb.append(" studyCodes: ").append(toIndentedString(studyCodes)).append("\n"); sb.append(" studyPUIs: ").append(toIndentedString(studyPUIs)).append("\n"); sb.append(" studyTypes: ").append(toIndentedString(studyTypes)).append("\n"); @@ -583,8 +571,6 @@ public Integer getTotalParameterCount() { count += this.seasonDbIds.size(); if (this.sortBy != null) count += 1; - if (this.sortOrder != null) - count += 1; if (this.studyCodes != null) count += this.studyCodes.size(); if (this.studyPUIs != null) @@ -593,4 +579,9 @@ public Integer getTotalParameterCount() { count += this.studyTypes.size(); return count; } + + @Override + public Map getEntityColAndTypeBySubmittedNameMap() { + return ALLOWED_SORT_AND_FILTER_FIELDS; + } } diff --git a/src/main/java/io/swagger/model/core/TrialSearchRequest.java b/src/main/java/io/swagger/model/core/TrialSearchRequest.java index 830dde51..2760e5d5 100644 --- a/src/main/java/io/swagger/model/core/TrialSearchRequest.java +++ b/src/main/java/io/swagger/model/core/TrialSearchRequest.java @@ -1,13 +1,32 @@ package io.swagger.model.core; -import java.util.Objects; +import java.util.*; + import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.model.SearchRequest; -import java.util.ArrayList; -import java.util.List; +import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; +import org.brapi.test.BrAPITestServer.model.dto.EntityType; + import java.time.LocalDate; public class TrialSearchRequest extends SearchRequest { + + // Key - allowed sort or field filter name for this entity + // Value = entity field name and type that represents the submitted field. Used later on in query building. + private static final Map ALLOWED_SORT_AND_FILTER_FIELDS = + Map.of( + "trialName", 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) + ); + @JsonProperty("commonCropNames") private List commonCropNames = null; @@ -50,28 +69,6 @@ public class TrialSearchRequest extends SearchRequest { @JsonProperty("trialPUIs") private List trialPUIs = null; - @JsonProperty("sortBy") - private SortBy sortBy = null; - - @JsonProperty("sortOrder") - private SortOrder sortOrder = null; - - public SortBy getSortBy() { - return sortBy; - } - - public void setSortBy(SortBy sortBy) { - this.sortBy = sortBy; - } - - public SortOrder getSortOrder() { - return sortOrder; - } - - public void setSortOrder(SortOrder sortOrder) { - this.sortOrder = sortOrder; - } - public TrialSearchRequest commonCropNames(List commonCropNames) { this.commonCropNames = commonCropNames; return this; @@ -445,4 +442,9 @@ public Integer getTotalParameterCount() { count += this.trialPUIs.size(); return count; } + + @Override + public Map getEntityColAndTypeBySubmittedNameMap() { + return ALLOWED_SORT_AND_FILTER_FIELDS; + } } diff --git a/src/main/java/io/swagger/model/sort/SortBy.java b/src/main/java/io/swagger/model/sort/SortBy.java new file mode 100644 index 00000000..bb286fa6 --- /dev/null +++ b/src/main/java/io/swagger/model/sort/SortBy.java @@ -0,0 +1,30 @@ +package io.swagger.model.sort; + +public class SortBy { + private String sortedOn; + private SortOrder sortOrder = SortOrder.ASC; + + public SortBy(String sortedOn, + SortOrder sortOrder) { + this.sortedOn = sortedOn; + this.sortOrder = sortOrder; + } + + public SortBy() {} + + public String getSortedOn() { + return sortedOn; + } + + public void setSortedOn(String sortedOn) { + this.sortedOn = sortedOn; + } + + public SortOrder getSortOrder() { + return sortOrder; + } + + public void setSortOrder(SortOrder sortOrder) { + this.sortOrder = sortOrder; + } +} diff --git a/src/main/java/io/swagger/model/core/SortOrder.java b/src/main/java/io/swagger/model/sort/SortOrder.java similarity index 92% rename from src/main/java/io/swagger/model/core/SortOrder.java rename to src/main/java/io/swagger/model/sort/SortOrder.java index 0e5b36bd..b8a85ebf 100644 --- a/src/main/java/io/swagger/model/core/SortOrder.java +++ b/src/main/java/io/swagger/model/sort/SortOrder.java @@ -1,4 +1,4 @@ -package io.swagger.model.core; +package io.swagger.model.sort; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; @@ -26,6 +26,6 @@ public static SortOrder fromValue(String text) { return b; } } - return null; + return ASC; } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/BrapiExceptionHandler.java b/src/main/java/org/brapi/test/BrAPITestServer/BrapiExceptionHandler.java index 7ee15a60..fa6508f0 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/BrapiExceptionHandler.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/BrapiExceptionHandler.java @@ -1,13 +1,12 @@ package org.brapi.test.BrAPITestServer; import java.util.ArrayList; +import java.util.Map; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.lang.Nullable; import org.springframework.web.bind.MissingServletRequestParameterException; @@ -70,4 +69,36 @@ private ResponseEntity buildErrorResponse(HttpStatus code, String messag return new ResponseEntity(apiError, code); } + + // This override handles JSON parsing failures detected by Jackson. + // It allows for capture of BrAPI generated BAD Request exceptions that are occur during serialization, + // like those involved in filtering and sorting in search requests. + @Override + protected ResponseEntity handleHttpMessageNotReadable( + HttpMessageNotReadableException ex, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request + ) { + Throwable root = ex.getMostSpecificCause(); + + if (root instanceof BrAPIServerException brapiServerException) { + + ProblemDetail detail = + ProblemDetail.forStatus(HttpStatus.BAD_REQUEST); + + detail.setTitle("Bad Request"); + detail.setDetail(brapiServerException.getResponseMessage()); + + return ResponseEntity.badRequest().body(detail); + } + + // Delegate back to Spring default behavior + return super.handleHttpMessageNotReadable( + ex, + headers, + status, + request + ); + } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityColumnNameAndType.java b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityColumnNameAndType.java new file mode 100644 index 00000000..71f4a9f2 --- /dev/null +++ b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityColumnNameAndType.java @@ -0,0 +1,28 @@ +package org.brapi.test.BrAPITestServer.model.dto; + +/** + * This class is used to map an entity's column name to the SQL type of lookup that should be completed for filter searches. + * The entityColumnName is also used for sorts as well. + */ +public class EntityColumnNameAndType { + String entityColumnName; + EntityType entityType; + + public EntityColumnNameAndType(String entityColumnName, EntityType entityType) { + this.entityColumnName = entityColumnName; + this.entityType = entityType; + } + + public String getEntityColumnName() { + return entityColumnName; + } + + public EntityType getEntityType() { + return entityType; + } + + @Override + public String toString() { + return this.entityColumnName; + } +} diff --git a/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityType.java b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityType.java new file mode 100644 index 00000000..e69aa988 --- /dev/null +++ b/src/main/java/org/brapi/test/BrAPITestServer/model/dto/EntityType.java @@ -0,0 +1,8 @@ +package org.brapi.test.BrAPITestServer.model.dto; + +public enum EntityType { + TEXT, + UUID, + BOOLEAN + // Add any other entity data types we should filter different in SearchQueryBuilder here +} diff --git a/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/TrialEntity.java b/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/TrialEntity.java index 1c9be57a..5f9bb989 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/TrialEntity.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/model/entity/core/TrialEntity.java @@ -4,8 +4,10 @@ import org.brapi.test.BrAPITestServer.model.entity.BrAPIPrimaryEntity; import org.brapi.test.BrAPITestServer.model.entity.pheno.ObservationEntity; import org.brapi.test.BrAPITestServer.model.entity.pheno.ObservationUnitEntity; +import org.hibernate.annotations.Formula; import org.hibernate.annotations.Where; +import java.time.OffsetDateTime; import java.util.Date; import java.util.List; @Entity @@ -38,6 +40,12 @@ public class TrialEntity extends BrAPIPrimaryEntity { @Column(name = "soft_deleted") private boolean softDeleted; + @Formula("(additional_info #>> '{createdDate}')") + private String createdDate; + + @Formula("(additional_info #>> '{createdBy,userName}')") + private String createdBy; + @ManyToOne(fetch = FetchType.LAZY) private CropEntity crop; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java index 80bf5bc5..d18a30a0 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java @@ -6,8 +6,13 @@ import java.time.LocalDate; import java.time.OffsetDateTime; +import io.swagger.model.FilterBy; import io.swagger.model.GeoJSONSearchArea; -import io.swagger.model.core.SortOrder; +import io.swagger.model.sort.SortBy; +import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; +import org.brapi.test.BrAPITestServer.model.dto.EntityColumnNameAndType; +import org.brapi.test.BrAPITestServer.model.dto.EntityType; +import org.springframework.http.HttpStatus; public class SearchQueryBuilder { @@ -17,6 +22,8 @@ public class SearchQueryBuilder { private String defaultSort; private String sortClause; private Map params; + private List joinedTables = new ArrayList<>(); + private List joinedFetchedTables = new ArrayList<>(); private Class clazz; public SearchQueryBuilder(Class clazz) { @@ -133,6 +140,26 @@ public SearchQueryBuilder appendSingle(UUID single, String columnName) { return this; } + public SearchQueryBuilder appendLike(String like, String columnName) { + String paramName = paramFilterPattern(columnName); + + if (like != null) { + this.whereClause += "AND lower(" + entityPrefix(columnName) + ") LIKE :" + paramName + " "; + this.params.put(paramName, "%" + like + "%"); + } + return this; + } + + public SearchQueryBuilder appendLikeIDs(String like, String columnName) { + String paramName = paramFilterPattern(columnName); + + if (like != null) { + this.whereClause += "AND cast(" + entityPrefix(columnName) + " as String) LIKE :" + paramName + " "; + this.params.put(paramName, "%" + like + "%"); + } + return this; + } + public > SearchQueryBuilder appendEnum(E enumVal, String columnName) { String paramName = paramFilter(columnName); if (enumVal != null) { @@ -258,13 +285,31 @@ public SearchQueryBuilder withExRefs(List exRefIds, List exRe } public SearchQueryBuilder join(String join, String name) { - this.selectClause += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; - this.selectOnlyIds += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; + + if (!this.joinedTables.contains(join) && !this.joinedFetchedTables.contains(join)) { + this.selectClause += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; + this.selectOnlyIds += "JOIN " + entityPrefix(join) + " " + paramFilter(name) + " "; + this.joinedTables.add(join); + } return this; } public SearchQueryBuilder leftJoinFetch(String join, String name) { - this.selectClause += generateLeftJoinFetch(join, name); + return leftJoinFetch(join, name, false); + } + + public SearchQueryBuilder leftJoinFetch(String join, String name, boolean overrideExistingJoin) { + + if (this.joinedTables.contains(join) && overrideExistingJoin) { + // Override existing normal join with join fetch if it already exists in a query + // This override does not change the alias to the name provided, and assumes further usages will use the same alias. + this.selectClause = this.selectClause.replace("JOIN " + entityPrefix(join), "LEFT JOIN FETCH " + entityPrefix(join)); + this.joinedFetchedTables.add(join); + this.joinedTables.remove(join); + } else if (!this.joinedFetchedTables.contains(join)) { + this.selectClause += generateLeftJoinFetch(join, name); + this.joinedFetchedTables.add(join); + } return this; } @@ -280,6 +325,9 @@ public SearchQueryBuilder removeAndReplaceLeftJoinFetch(String join, this.selectClause = this.selectClause.replace(generateLeftJoinFetch(existingJoin, existingName), generateLeftJoinFetch(join, name)); + this.joinedFetchedTables.remove(existingJoin); + this.joinedFetchedTables.add(join); + return this; } @@ -290,6 +338,7 @@ public SearchQueryBuilder removeAndReplaceLeftJoinFetch(String join, public SearchQueryBuilder removeLeftJoinFetch(String join, String name) { this.selectClause = this.selectClause.replace(generateLeftJoinFetch(join, name), ""); + this.joinedFetchedTables.remove(join); return this; } @@ -311,14 +360,108 @@ private String paramFilter(String param) { return param.replace('.', '_').replace('*', '_'); } - public SearchQueryBuilder withSort(String sortByStr, SortOrder sortOrder) { - String sortOrderStr = "ASC"; - if (sortOrder != null) { - sortOrderStr = sortOrder.toString(); + private String paramFilterPattern(String param) { + if (param == null) + return ""; + return param.replace('.', '_').replace('*', '_') + "Pattern"; + } + + /** + * Takes a list of SortBy options that should typically come in a searchRequest, along with a map of the validated + * columns names. + * Applies the entries in the list to sort the SearchQuery. + * + * A SortBy has + * - A column name + * - An order (DESC, ASC) + */ + public SearchQueryBuilder sortBy(List sortBy, Map entityColAndTypeBySubmittedName) throws BrAPIServerException { + + if (sortBy == null || sortBy.isEmpty()) { + return this; } - this.sortClause += " ORDER BY " + entityPrefix(sortByStr) + " " + sortOrderStr; + for (SortBy sort : sortBy) { + // At this point, the submitted sortBy name has been verified to be in entityColAndTypeBySubmittedName + EntityColumnNameAndType entityColumnNameAndType = entityColAndTypeBySubmittedName.get(sort.getSortedOn()); + + String entityColName = entityColumnNameAndType.getEntityColumnName(); + + if (entityColName.startsWith("*")) { + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "Sorting on one to many relationships not supported"); + } + + String[] split = entityColName.split("\\."); + + if (split.length > 2) { + // TODO: Implement this if it becomes a requirement + throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "Sorting on a table greater than one level from primary entity not allowed"); + } + + if (split.length == 2 && !split[1].equals("id")) { + leftJoinFetch(split[0], split[0], true); + } + + sort.setSortedOn(entityColName); + + if (sortBy.getFirst().equals(sort)) { + this.sortClause += " ORDER BY "; + buildSort(sort); + } else { + this.sortClause += ", "; + buildSort(sort); + } + } return this; } + + private void buildSort(SortBy sort) { + this.sortClause += entityPrefix(sort.getSortedOn()) + " " + sort.getSortOrder() + " "; + } + + /** + * Takes a list of FilterBy options that should typically come in a searchRequest, along with a map of the validated + * columns names and the data type they represent for accurate filtering on different data types. + * Applies the entries in the list to filter the SearchQuery. + * + * A FilterBy has + * - A column name + * - A value which the column name should be filtered on + */ + public SearchQueryBuilder filterBy(List filterBy, Map entityColAndTypeBySubmittedName) throws BrAPIServerException { + SearchQueryBuilder searchQuery = this; + + if (filterBy == null || filterBy.isEmpty()) { + return searchQuery; + } + + for (FilterBy filter : filterBy) { + // At this point, the submitted filterBy column name has been verified to be in entityColAndTypeBySubmittedName + EntityColumnNameAndType entityColumnNameAndType = entityColAndTypeBySubmittedName.get(filter.getFilterOn()); + + if (entityColumnNameAndType.getEntityColumnName().startsWith("*")) { + joinCollectionColumn(entityColumnNameAndType.getEntityColumnName()); + } + + if (entityColumnNameAndType.getEntityType() == EntityType.TEXT) { + searchQuery = appendLike(filter.getValue().toLowerCase(), entityColumnNameAndType.getEntityColumnName()); + } else if (entityColumnNameAndType.getEntityType() == EntityType.UUID) { + searchQuery = appendLikeIDs(filter.getValue(), entityColumnNameAndType.getEntityColumnName()); + } + } + + return searchQuery; + } + + /** + * This helper method joins the table that a collection column name is related to if it doesn't exist already. + * This is particularly important for filter search requests because if the join doesn't exist, and it is referenced + * the query will not execute. + */ + private void joinCollectionColumn(String submittedSortFilterColumnName) { + String joinTableName = submittedSortFilterColumnName.substring(1, submittedSortFilterColumnName.indexOf(".")); + + this.join(joinTableName, joinTableName); + } } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java index 1b6526ea..ae6a297b 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java @@ -3,10 +3,10 @@ import java.util.*; import java.util.stream.Collectors; +import io.swagger.model.sort.SortBy; import org.apache.commons.lang3.StringUtils; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerDbIdNotFoundException; import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException; -import org.brapi.test.BrAPITestServer.model.entity.BrAPIBaseEntity; import org.brapi.test.BrAPITestServer.model.entity.core.CropEntity; import org.brapi.test.BrAPITestServer.model.entity.core.DataLinkEntity; import org.brapi.test.BrAPITestServer.model.entity.core.EnvironmentParametersEntity; @@ -38,8 +38,7 @@ import io.swagger.model.Metadata; import io.swagger.model.core.Contact; import io.swagger.model.core.EnvironmentParameter; -import io.swagger.model.core.SortBy; -import io.swagger.model.core.SortOrder; +import io.swagger.model.sort.SortOrder; import io.swagger.model.core.Study; import io.swagger.model.core.StudyExperimentalDesign; import io.swagger.model.core.StudyGrowthFacility; @@ -108,10 +107,11 @@ public List findStudies(String commonCropName, String studyType, String p request.addObservationVariableDbIdsItem(observationVariableDbId); if (active != null) request.setActive(active); - if (sortBy != null && SortBy.fromValue(sortBy) != null) - request.setSortBy(SortBy.fromValue(sortBy)); - if (sortOrder != null && SortOrder.fromValue(sortOrder) != null) - request.setSortOrder(SortOrder.fromValue(sortOrder)); + if (sortBy != null) { + SortBy sortByElement = new SortBy(sortBy, SortOrder.fromValue(sortOrder)); + + request.setSortBy(List.of(sortByElement)); + } request.addExternalReferenceItem(externalReferenceId, externalReferenceID, externalReferenceSource); @@ -159,7 +159,7 @@ public List findStudies(StudySearchRequest request, Metadata metaData) .appendList(request.getStudyDbIds(), "id").appendList(request.getStudyNames(), "studyName") .appendList(request.getStudyPUIs(), "studyPUI").appendList(request.getStudyTypes(), "studyType") .appendList(request.getTrialDbIds(), "trial.id").appendList(request.getTrialNames(), "trial.trialName") - .withSort(getSortByField(request.getSortBy()), request.getSortOrder()); + .sortBy(request.getSortByElements(), request.getEntityColAndTypeBySubmittedNameMap()); Page studiesPage = studyRepository.findAllBySearchAndPaginate(searchQuery, pageReq); PagingUtility.calculateMetaData(metaData, studiesPage); @@ -566,47 +566,4 @@ private EnvironmentParametersEntity convertToEntity(EnvironmentParameter param) return entity; } - private String getSortByField(SortBy sortBy) { - String sortByStr = "id"; - if (sortBy != null) { - switch (sortBy) { - case GERMPLASMDBID: - sortByStr = "*obsunit.germplasm.id"; - break; - case LOCATIONDBID: - sortByStr = "location.id"; - break; - case OBSERVATIONVARIABLEDBID: - sortByStr = "*observation.observationVariable.id"; - break; - case PROGRAMDBID: - sortByStr = "trial.program.id"; - break; - case PROGRAMNAME: - sortByStr = "trial.program.name"; - break; - case SEASONDBID: - sortByStr = "*season.id"; - break; - case STUDYDBID: - sortByStr = "id"; - break; - case STUDYLOCATION: - sortByStr = "location.id"; - break; - case TRIALDBID: - sortByStr = "trial.id "; - break; - case STUDYTYPE: - sortByStr = "studyName"; - break; - case STUDYNAME: - default: - sortByStr = "studyName"; - break; - } - } - return sortByStr; - } - } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java index fb798a5b..21c30c65 100644 --- a/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java +++ b/src/main/java/org/brapi/test/BrAPITestServer/service/core/TrialService.java @@ -4,6 +4,8 @@ import java.util.stream.Collectors; import io.swagger.model.core.*; +import io.swagger.model.sort.SortBy; +import io.swagger.model.sort.SortOrder; import jakarta.validation.Valid; import org.brapi.test.BrAPITestServer.exceptions.BatchDeleteWrongTypeException; @@ -94,11 +96,10 @@ public List findTrials(@Valid String commonCropName, @Valid String contac request.setSearchDateRangeStart(searchDateRangeStart); if (searchDateRangeEnd != null) request.setSearchDateRangeEnd(searchDateRangeEnd); - if (sortBy != null && SortBy.fromValue(sortBy) != null) - request.setSortBy(SortBy.fromValue(sortBy)); - if (sortOrder != null && SortOrder.fromValue(sortOrder) != null) - request.setSortOrder(SortOrder.fromValue(sortOrder)); - + if (sortBy != null) { + SortBy querySortBy = new SortBy(sortBy, SortOrder.fromValue(sortOrder)); + request.setSortBy(List.of(querySortBy)); + } request.addExternalReferenceItem(externalReferenceId, externalReferenceID, externalReferenceSource); return findTrials(request, metadata); } @@ -113,20 +114,21 @@ public List findTrials(@Valid TrialSearchRequest request, Metadata metada searchQuery = searchQuery.join("contacts", "contact"); } if (request.getStudyDbIds() != null || request.getStudyNames() != null) { - searchQuery = searchQuery.join("studies", "study"); + searchQuery = searchQuery.join("studies", "studies"); } searchQuery = searchQuery.withExRefs(request.getExternalReferenceIDs(), request.getExternalReferenceSources()) .appendList(request.getCommonCropNames(), "crop.cropName") .appendList(request.getContactDbIds(), "*contact.id") - .appendList(request.getLocationDbIds(), "*study.location.id") - .appendList(request.getLocationNames(), "*study.location.locationName") + .appendList(request.getLocationDbIds(), "*studies.location.id") + .appendList(request.getLocationNames(), "*studies.location.locationName") .appendList(request.getProgramDbIds(), "program.id") - .appendList(request.getProgramNames(), "program.name").appendList(request.getStudyDbIds(), "*study.id") - .appendList(request.getStudyNames(), "*study.studyName").appendList(request.getTrialDbIds(), "id") + .appendList(request.getProgramNames(), "program.name").appendList(request.getStudyDbIds(), "*studies.id") + .appendList(request.getStudyNames(), "*studies.studyName").appendList(request.getTrialDbIds(), "id") .appendList(request.getTrialNames(), "trialName") .appendDateRange(request.getSearchDateRangeStart(), request.getSearchDateRangeEnd(), "startDate") - .withSort(getSortByField(request.getSortBy()), request.getSortOrder()); + .sortBy(request.getSortByElements(), request.getEntityColAndTypeBySubmittedNameMap()) + .filterBy(request.getFilterBy(), request.getEntityColAndTypeBySubmittedNameMap()); Page trialsPage = trialRepository.findAllBySearchAndPaginate(searchQuery, pageReq); PagingUtility.calculateMetaData(metadata, trialsPage); @@ -356,34 +358,4 @@ private PublicationEntity convertToEntity(TrialNewRequestPublications pub) { return entity; } - - private String getSortByField(SortBy sortBy) { - String sortByStr = "id"; - if (sortBy != null) { - switch (sortBy) { - case STARTDATE: - sortByStr = "startDate"; - break; - case ENDDATE: - sortByStr = "endDate"; - break; - case TRIALNAME: - sortByStr = "trialName"; - break; - case PROGRAMDBID: - sortByStr = "program.id"; - break; - case PROGRAMNAME: - sortByStr = "program.name"; - break; - case TRIALDBID: - default: - sortByStr = "id"; - break; - } - } - - return sortByStr; - } - } diff --git a/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java b/src/main/java/org/brapi/test/BrAPITestServer/service/germ/GermplasmService.java index 2946fd58..563cdc1b 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 @@ -151,6 +151,7 @@ public Page findGermplasmEntities(@Valid GermplasmSearchRequest return germs; } + // TODO: Investigate/consider removing this code. It is largely unused by DeltaBreed now, as paginating is the least memory exhaustive option. [BI-3020] public List findGermplasmEntitiesWithoutPaging(@Valid GermplasmSearchRequest request) { SearchQueryBuilder searchQuery = buildGermplasmSearchQuery(request);