From 0f8a05db9f3eb42e745cc13251973a2dac856d90 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 15 Jun 2026 19:08:46 -0400 Subject: [PATCH 1/3] Generalize sorting and filtering code for reusability for future cache removal --- .../v1/model/request/query/BrapiQuery.java | 23 ++++++++++ .../brapi/v2/BrAPITrialsController.java | 1 - .../brapi/v2/dao/impl/BrAPITrialDAOImpl.java | 35 +-------------- .../model/request/query/ExperimentQuery.java | 44 ++++++++++--------- .../utilities/BrAPIDAOUtil.java | 42 ++++++++++++++++-- 5 files changed, 86 insertions(+), 59 deletions(-) diff --git a/src/main/java/org/breedinginsight/brapi/v1/model/request/query/BrapiQuery.java b/src/main/java/org/breedinginsight/brapi/v1/model/request/query/BrapiQuery.java index f51bcf317..f87468ef3 100644 --- a/src/main/java/org/breedinginsight/brapi/v1/model/request/query/BrapiQuery.java +++ b/src/main/java/org/breedinginsight/brapi/v1/model/request/query/BrapiQuery.java @@ -26,6 +26,7 @@ import javax.validation.constraints.Positive; import javax.validation.constraints.PositiveOrZero; +import java.util.Map; @Getter @Setter @@ -58,4 +59,26 @@ protected FilterRequest constructFilterRequest(String field, String value) { .value(value) .build(); } + + /** + * For BrapiQuery objects that have implemented this method, this map is used by the BrAPIDAOUtil + * to iterate through all valid filter columns and their associated values that are submitted from the fe. + * + * See @ExperimentQuery for example implementation + */ + public Map getFilterValuesByBrAPIColumnName() { + throw new UnsupportedOperationException( + String.format("Server side BrAPI filtering not implemented for class [%s]", this.getClass().getSimpleName()) + ); + } + + /** + * For BrapiQuery objects that have implemented this method, this map is used by the BrAPIDAOUtil + * to match submitted sortField column names to the associated brapi column name for sorting. + * + * See @ExperimentQuery for example implementation + */ + public Map getBrAPIColumnNamesByBiColumnName() { + throw new UnsupportedOperationException(String.format("Server side BrAPI sorting not implemented for class [%s]", this.getClass().getSimpleName())); + } } diff --git a/src/main/java/org/breedinginsight/brapi/v2/BrAPITrialsController.java b/src/main/java/org/breedinginsight/brapi/v2/BrAPITrialsController.java index 6a2cd579f..be42a5782 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/BrAPITrialsController.java +++ b/src/main/java/org/breedinginsight/brapi/v2/BrAPITrialsController.java @@ -93,7 +93,6 @@ public HttpResponse>> getExperiments( Optional program = programService.getById(programId); if (program.isEmpty()) { return HttpResponse.notFound(); } - SearchRequest searchRequest = queryParams.constructSearchRequest(); log.debug("fetching trials for program: " + programId); // If the program user is an experimental collaborator, filter results for only authorized experiments. diff --git a/src/main/java/org/breedinginsight/brapi/v2/dao/impl/BrAPITrialDAOImpl.java b/src/main/java/org/breedinginsight/brapi/v2/dao/impl/BrAPITrialDAOImpl.java index b5465830d..f38f03073 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/dao/impl/BrAPITrialDAOImpl.java +++ b/src/main/java/org/breedinginsight/brapi/v2/dao/impl/BrAPITrialDAOImpl.java @@ -27,7 +27,6 @@ import org.brapi.client.v2.model.queryParams.core.TrialQueryParams; import org.brapi.client.v2.modules.core.TrialsApi; import org.brapi.v2.model.BrAPIFilterBy; -import org.brapi.v2.model.BrAPIResponse; import org.brapi.v2.model.BrAPISortBy; import org.brapi.v2.model.core.BrAPIProgram; import org.brapi.v2.model.core.BrAPITrial; @@ -284,40 +283,8 @@ private BrAPITrialSearchRequest buildSearchRequest(Program program, List b searchRequest.setTrialDbIds(brapiTrialIds.stream().map(UUID::toString).collect(Collectors.toList())); } - // Set FilterBy - List brAPIFilterBy = new ArrayList<>(); - - if (StringUtils.isNotBlank(experimentQuery.getName())) { - brAPIFilterBy.add(brAPIDAOUtil.constructFilterBy(brAPITrialsMapper.getBrAPIName(BI_FILTER_COLUMN_NAME), experimentQuery.getName())); - } - if (StringUtils.isNotBlank(experimentQuery.getActive())) { - brAPIFilterBy.add(brAPIDAOUtil.constructFilterBy(brAPITrialsMapper.getBrAPIName(BI_FILTER_COLUMN_ACTIVE), experimentQuery.getActive())); - } - if (StringUtils.isNotBlank(experimentQuery.getCreatedBy())) { - brAPIFilterBy.add(brAPIDAOUtil.constructFilterBy(brAPITrialsMapper.getBrAPIName(BI_FILTER_COLUMN_CREATED_BY), experimentQuery.getCreatedBy())); - } - if (StringUtils.isNotBlank(experimentQuery.getCreatedDate())) { - brAPIFilterBy.add(brAPIDAOUtil.constructFilterBy(brAPITrialsMapper.getBrAPIName(BI_FILTER_COLUMN_CREATED_DATE), experimentQuery.getCreatedDate())); - } - - if (!brAPIFilterBy.isEmpty()) { - searchRequest.setFilterBy(brAPIFilterBy); - } - - // Set SortBy - List brAPISortBy = new ArrayList<>(); - - if (StringUtils.isNotBlank(experimentQuery.getSortField()) && brAPITrialsMapper.exists(experimentQuery.getSortField())) { - brAPISortBy.add(brAPIDAOUtil.constructSortBy(brAPITrialsMapper.getBrAPIName(experimentQuery.getSortField()), experimentQuery.getSortOrder().toString())); - } - - if (!brAPISortBy.isEmpty()) { - searchRequest.setSortBy(brAPISortBy); - } - - brAPIDAOUtil.setPagination(searchRequest, experimentQuery); + brAPIDAOUtil.setGenericSearchParameters(searchRequest, experimentQuery); return searchRequest; - } } diff --git a/src/main/java/org/breedinginsight/brapi/v2/model/request/query/ExperimentQuery.java b/src/main/java/org/breedinginsight/brapi/v2/model/request/query/ExperimentQuery.java index b4c08c31f..02f025968 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/model/request/query/ExperimentQuery.java +++ b/src/main/java/org/breedinginsight/brapi/v2/model/request/query/ExperimentQuery.java @@ -2,13 +2,10 @@ import io.micronaut.core.annotation.Introspected; import lombok.Getter; -import org.breedinginsight.api.model.v1.request.query.FilterRequest; -import org.breedinginsight.api.model.v1.request.query.SearchRequest; import org.breedinginsight.brapi.v1.model.request.query.BrapiQuery; -import org.jooq.tools.StringUtils; -import java.util.ArrayList; -import java.util.List; +import java.util.HashMap; +import java.util.Map; @Getter @Introspected @@ -18,20 +15,27 @@ public class ExperimentQuery extends BrapiQuery { private String createdBy; private String createdDate; - public SearchRequest constructSearchRequest() { - List filters = new ArrayList<>(); - if (!StringUtils.isBlank(getName())) { - filters.add(constructFilterRequest("name", getName())); - } - if (!StringUtils.isBlank(getActive())) { - filters.add(constructFilterRequest("active", getActive())); - } - if (!StringUtils.isBlank(getCreatedBy())) { - filters.add(constructFilterRequest("createdBy", getCreatedBy())); - } - if (!StringUtils.isBlank(getCreatedDate())) { - filters.add(constructFilterRequest("createdDate", getCreatedDate())); - } - return new SearchRequest(filters); + @Override + public Map getFilterValuesByBrAPIColumnName() { + Map filterValuesByBrAPIColumnName = new HashMap<>(); + + filterValuesByBrAPIColumnName.put("trialName", getName()); + filterValuesByBrAPIColumnName.put("active", getActive()); + filterValuesByBrAPIColumnName.put("createdBy", getCreatedBy()); + filterValuesByBrAPIColumnName.put("createdDate", getCreatedDate()); + + return filterValuesByBrAPIColumnName; + } + + @Override + public Map getBrAPIColumnNamesByBiColumnName() { + Map brAPIColumnNamesByBiColumnName = new HashMap<>(); + + brAPIColumnNamesByBiColumnName.put("name", "trialName"); + brAPIColumnNamesByBiColumnName.put("active", "active"); + brAPIColumnNamesByBiColumnName.put("createdBy", "createdBy"); + brAPIColumnNamesByBiColumnName.put("createdDate", "createdDate"); + + return brAPIColumnNamesByBiColumnName; } } diff --git a/src/main/java/org/breedinginsight/utilities/BrAPIDAOUtil.java b/src/main/java/org/breedinginsight/utilities/BrAPIDAOUtil.java index 947625656..7290621a2 100644 --- a/src/main/java/org/breedinginsight/utilities/BrAPIDAOUtil.java +++ b/src/main/java/org/breedinginsight/utilities/BrAPIDAOUtil.java @@ -33,7 +33,6 @@ import org.brapi.client.v2.ApiResponse; import org.brapi.client.v2.model.exceptions.ApiException; import org.brapi.v2.model.*; -import org.breedinginsight.api.model.v1.response.DataResponse; import org.breedinginsight.brapi.v1.controller.BrapiVersion; import org.breedinginsight.brapi.v1.model.request.query.BrapiQuery; import org.breedinginsight.brapps.importer.model.ImportUpload; @@ -506,7 +505,7 @@ public String getProgramBrAPIBaseUrl(UUID programId) { return programBrAPIBaseUrl.endsWith(BrapiVersion.BRAPI_V2) ? programBrAPIBaseUrl : programBrAPIBaseUrl + BrapiVersion.BRAPI_V2; } - public BrAPIFilterBy constructFilterBy(String filterOn, String value) { + private BrAPIFilterBy constructFilterBy(String filterOn, String value) { var filterBy = new BrAPIFilterBy(); filterBy.setFilterOn(filterOn); @@ -514,7 +513,7 @@ public BrAPIFilterBy constructFilterBy(String filterOn, String value) { return filterBy; } - public BrAPISortBy constructSortBy(String sortOn, String sortOrder) { + private BrAPISortBy constructSortBy(String sortOn, String sortOrder) { var sortBy = new BrAPISortBy(); sortBy.setSortedOn(sortOn); @@ -529,7 +528,42 @@ public BrAPISortBy constructSortBy(String sortOn, String sortOrder) { return sortBy; } - public void setPagination(T brapiSearchRequest, U biSearchQuery) { + /** + * Sets three generic search parameters for an outgoing BrAPI Search Query: + * - Sorting + * - Filtering + * - Pagination + */ + public void setGenericSearchParameters(T brapiSearchRequest, U biSearchQuery) { + // Set SortBy + List brAPISortBy = new ArrayList<>(); + + Map brapiColNameByBiColName = biSearchQuery.getBrAPIColumnNamesByBiColumnName(); + + if (StringUtils.isNotBlank(biSearchQuery.getSortField()) && brapiColNameByBiColName.containsKey(biSearchQuery.getSortField())) { + brAPISortBy.add(constructSortBy(brapiColNameByBiColName.get(biSearchQuery.getSortField()), biSearchQuery.getSortOrder().toString())); + } + + if (!brAPISortBy.isEmpty()) { + brapiSearchRequest.setSortBy(brAPISortBy); + } + + // Set FilterBy + List brAPIFilterBy = new ArrayList<>(); + + Map filterValuesByBrAPIColName = biSearchQuery.getFilterValuesByBrAPIColumnName(); + + filterValuesByBrAPIColName.forEach((brapiColumnName, value) -> { + if (StringUtils.isNotBlank(value)) { + brAPIFilterBy.add(constructFilterBy(brapiColumnName, value)); + } + }); + + if (!brAPIFilterBy.isEmpty()) { + brapiSearchRequest.setFilterBy(brAPIFilterBy); + } + + // Set Pagination if (biSearchQuery.getPage() == null) { brapiSearchRequest.setPage(biSearchQuery.getDefaultPage()); } else { From 8067d5c1105c27e8edad56effdf2018ddcb0cc6a Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Tue, 16 Jun 2026 20:08:26 -0400 Subject: [PATCH 2/3] Create generalized brapi get method --- .../brapi/v2/dao/impl/BrAPITrialDAOImpl.java | 38 ++++++-------- .../utilities/BrAPIDAOUtil.java | 50 +++++++++++++++++++ 2 files changed, 64 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/breedinginsight/brapi/v2/dao/impl/BrAPITrialDAOImpl.java b/src/main/java/org/breedinginsight/brapi/v2/dao/impl/BrAPITrialDAOImpl.java index f38f03073..a34857f56 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/dao/impl/BrAPITrialDAOImpl.java +++ b/src/main/java/org/breedinginsight/brapi/v2/dao/impl/BrAPITrialDAOImpl.java @@ -103,32 +103,13 @@ private List getBrAPITrialsUsingBrAPIProgramId(Program program) thro TrialQueryParams trialQueryParams = TrialQueryParams.builder() .programDbId(brapiProgramDbId) - .pageSize(1000) - .page(0) .build(); - return getTrialsFromBrAPI(program, trialQueryParams); - } - - private List getTrialsFromBrAPI(Program program, TrialQueryParams trialQueryParams) throws ApiException { TrialsApi api = brAPIEndpointProvider.get(programDAO.getCoreClient(program.getId()), TrialsApi.class); - ApiResponse response; - - try { - response = api.trialsGet(trialQueryParams); - } catch (ApiException e) { - log.warn(Utilities.generateApiExceptionLogMessage(e)); - throw new InternalServerException("Error making BrAPI call", e); - } - - if (response.getBody().getMetadata().getPagination().getTotalCount() > trialQueryParams.pageSize()) { - throw new InternalServerException(String.format("More trials exist than requested [%s]", trialQueryParams)); - } + List result = brAPIDAOUtil.get(api::trialsGet, trialQueryParams); - List trialsFromResponse = response.getBody().getResult().getData(); - - return processExperimentsForDisplay(trialsFromResponse, program.getKey()); + return processExperimentsForDisplay(result, program.getKey()); } @Override @@ -192,12 +173,21 @@ public Optional getTrialById(UUID programId, UUID trialId) throws Ap TrialQueryParams params = TrialQueryParams.builder() .trialDbId(trialId.toString()) - .pageSize(1) - .page(0) .build(); + TrialsApi api = brAPIEndpointProvider.get(programDAO.getCoreClient(programId), TrialsApi.class); + + List result = brAPIDAOUtil.get(api::trialsGet, params); + + if (result.isEmpty()) { + throw new DoesNotExistException(String.format("Trial with ID [%s] does not exist", trialId)); + } else if (result.size() > 1) { + throw new InternalServerException(String.format("Trial with ID [%s] found multiple times in database. This should not be possible.", trialId)); + } + + processExperimentsForDisplay(result, program.getKey()); - return getTrialsFromBrAPI(program, params).stream().findFirst(); + return result.stream().findFirst(); } @Override diff --git a/src/main/java/org/breedinginsight/utilities/BrAPIDAOUtil.java b/src/main/java/org/breedinginsight/utilities/BrAPIDAOUtil.java index 7290621a2..f7c6e422d 100644 --- a/src/main/java/org/breedinginsight/utilities/BrAPIDAOUtil.java +++ b/src/main/java/org/breedinginsight/utilities/BrAPIDAOUtil.java @@ -32,6 +32,7 @@ import org.apache.commons.lang3.tuple.Pair; import org.brapi.client.v2.ApiResponse; import org.brapi.client.v2.model.exceptions.ApiException; +import org.brapi.client.v2.model.queryParams.core.BrAPIQueryParams; import org.brapi.v2.model.*; import org.breedinginsight.brapi.v1.controller.BrapiVersion; import org.breedinginsight.brapi.v1.model.request.query.BrapiQuery; @@ -75,6 +76,55 @@ public BrAPIDAOUtil(@Property(name = "brapi.search.wait-time") int searchWaitTim this.programService = programService; } + public List get(Function> getMethod, + U queryParams) throws ApiException { + T brapiResponseResult; + + boolean paginationSet = queryParams.page() != null && queryParams.pageSize() != null; + + if (!paginationSet) { + queryParams.setPage(0); + queryParams.setPageSize(pageSize); + } + + try { + brapiResponseResult = Optional.ofNullable(getMethod.apply(queryParams)) + .map(ApiResponse::getBody) + .orElseThrow(); + } catch (ApiException e) { + log.warn(Utilities.generateApiExceptionLogMessage(e)); + throw e; + } catch (NoSuchElementException e) { + log.debug("Unable to retrieve BrAPIResponse from POST search request", e); + throw new InternalServerException(e.toString(), e); + } catch (Exception e) { + log.debug("error", e); + throw new InternalServerException(e.toString(), e); + } + + BrAPIPagination responsePagination = Optional.of(brapiResponseResult) + .map(BrAPIResponse::getMetadata) + .map(BrAPIMetadata::getPagination) + .orElse(null); + + if (responsePagination == null) { + throw new InternalServerException("Expected pagination metadata in BrAPI get response not present"); + } + + if (!paginationSet && responsePagination.getTotalPages() > 1) { + // Size of BrAPI entity GET data can not exceed configured pageSize. + // If it does, it is recommended the GET method not be used for this purpose. + // If you are trying to utilize this method to get a large amount of data for an entity, you can use + // searchInternal for now. In the future, we need a more robust way of transmitting this data using + // compression client techniques to reduce strain on the server and avoid keeping all the database data + // in java memory. + throw new InternalServerException(String.format("More than one page found, meaning there is more data to be returned " + + "with pageSize = [%s]", queryParams.pageSize())); + } + + return getListResult(brapiResponseResult); + } + // Performs a POST brapi search on an entity without looping paging logic, utilizing filters and pagination provided in the searchBody. // Also verifies response paging matches requested paging public T simpleSearch(Function, Optional>>> searchMethod, From a470ef99273e04a749a6fb50c5fe6017c0f4a280 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Wed, 17 Jun 2026 12:19:05 -0400 Subject: [PATCH 3/3] Update comments --- .../brapi/v2/dao/impl/BrAPITrialDAOImpl.java | 3 --- .../java/org/breedinginsight/utilities/BrAPIDAOUtil.java | 8 -------- 2 files changed, 11 deletions(-) diff --git a/src/main/java/org/breedinginsight/brapi/v2/dao/impl/BrAPITrialDAOImpl.java b/src/main/java/org/breedinginsight/brapi/v2/dao/impl/BrAPITrialDAOImpl.java index a34857f56..96d7e9ff7 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/dao/impl/BrAPITrialDAOImpl.java +++ b/src/main/java/org/breedinginsight/brapi/v2/dao/impl/BrAPITrialDAOImpl.java @@ -82,7 +82,6 @@ public BrAPITrialDAOImpl(ProgramDAO programDAO, * This method requires a BI-API program. If the BrAPIProgram inside this data model is not set, * this method will retrieve it. */ - // TODO: Generalize Code for General Get By Program for BrAPI Entities [BI-2932] private List getBrAPITrialsUsingBrAPIProgramId(Program program) throws ApiException { if (program == null || program.getId() == null) { @@ -98,8 +97,6 @@ private List getBrAPITrialsUsingBrAPIProgramId(Program program) thro brapiProgramDbId = programDAO.getProgramBrAPI(program).getProgramDbId(); } - // TODO: Configurable max amount of trials per program, or paginate [BI-2932] - TrialQueryParams trialQueryParams = TrialQueryParams.builder() .programDbId(brapiProgramDbId) diff --git a/src/main/java/org/breedinginsight/utilities/BrAPIDAOUtil.java b/src/main/java/org/breedinginsight/utilities/BrAPIDAOUtil.java index f7c6e422d..0917998fb 100644 --- a/src/main/java/org/breedinginsight/utilities/BrAPIDAOUtil.java +++ b/src/main/java/org/breedinginsight/utilities/BrAPIDAOUtil.java @@ -401,14 +401,6 @@ public List getListResult(T response) { .orElseThrow(); } - // TODO: write generic put code - public List put(List brapiObjects, - ImportUpload upload, - Function, ApiResponse> putMethod, - Consumer progressUpdateMethod) throws ApiException { - throw new UnsupportedOperationException(); - } - public List post(List brapiObjects, ImportUpload upload, Function, ApiResponse> postMethod,