From 58d65e69bff0fc5ec1adbe1cf2dcae683e0248aa Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Thu, 24 Sep 2026 20:20:24 -0400 Subject: [PATCH] Implement brapi side filtering, sorting, paging for germplasm --- .../brapi/v2/BrAPIGermplasmController.java | 13 ++- .../brapi/v2/dao/BrAPIGermplasmDAO.java | 72 +++++++-------- .../model/request/query/GermplasmQuery.java | 40 +++++++++ .../v2/services/BrAPIGermplasmService.java | 89 +++++++++++++++---- .../GermplasmControllerIntegrationTest.java | 1 + 5 files changed, 159 insertions(+), 56 deletions(-) diff --git a/src/main/java/org/breedinginsight/brapi/v2/BrAPIGermplasmController.java b/src/main/java/org/breedinginsight/brapi/v2/BrAPIGermplasmController.java index f234c3030..2d39cf251 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/BrAPIGermplasmController.java +++ b/src/main/java/org/breedinginsight/brapi/v2/BrAPIGermplasmController.java @@ -19,6 +19,7 @@ import org.brapi.v2.model.BrAPIIndexPagination; import org.brapi.v2.model.BrAPIMetadata; import org.brapi.v2.model.BrAPIStatus; +import org.brapi.v2.model.core.BrAPITrial; import org.brapi.v2.model.germ.*; import org.brapi.v2.model.germ.request.BrAPIGermplasmSearchRequest; import org.brapi.v2.model.germ.response.BrAPIGermplasmListResponse; @@ -185,7 +186,7 @@ private void batchProcessGermplasm(List germplasmList, String pr @Get("/programs/{programId}" + BrapiVersion.BRAPI_V2 + "/germplasm{?queryParams*}") @Produces(MediaType.APPLICATION_JSON) @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.PROGRAM_SCOPED_ROLES}) - public HttpResponse>>> getGermplasm( + public HttpResponse>> getGermplasm( @PathVariable("programId") UUID programId, @QueryValue @QueryValid(using = GermplasmQueryMapper.class) @Valid GermplasmQuery queryParams) { try { @@ -198,15 +199,19 @@ public HttpResponse>>> getGermplasm( } // Fetch all germplasm in the program unless a list id is supplied to return only germplasm in that collection - List germplasm = queryParams.getListDbId() == null ? germplasmService.getGermplasm(programId) : germplasmService.getGermplasmByList(programId, queryParams.getListDbId()); - SearchRequest searchRequest = queryParams.constructSearchRequest(); - return ResponseUtils.getBrapiQueryResponse(germplasm, germplasmQueryMapper, queryParams, searchRequest); + BrAPIGermplasmListResponse brapiResponse = queryParams.getListDbId() == null ? germplasmService.searchGermplasm(programId, queryParams) : germplasmService.getGermplasmByList(programId, queryParams); + + List foundTrials = brapiResponse.getResult().getData(); + return ResponseUtils.getBrapiQueryResponse(foundTrials, brapiResponse, queryParams); } catch (ApiException e) { log.info(e.getMessage(), e); return HttpResponse.status(HttpStatus.INTERNAL_SERVER_ERROR, "Error retrieving germplasm"); } catch (IllegalArgumentException e) { log.info(e.getMessage(), e); return HttpResponse.status(HttpStatus.UNPROCESSABLE_ENTITY, "Error parsing requested date format"); + } catch (DoesNotExistException e) { + log.info(e.getMessage(), e); + return HttpResponse.status(HttpStatus.NOT_FOUND, "Supplied programId does not exist"); } } diff --git a/src/main/java/org/breedinginsight/brapi/v2/dao/BrAPIGermplasmDAO.java b/src/main/java/org/breedinginsight/brapi/v2/dao/BrAPIGermplasmDAO.java index a90d46bc8..998662370 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/dao/BrAPIGermplasmDAO.java +++ b/src/main/java/org/breedinginsight/brapi/v2/dao/BrAPIGermplasmDAO.java @@ -135,16 +135,15 @@ public List getRawGermplasm(UUID programId) throws ApiException /** * Process germplasm into a format for display - * @param programGermplasm + * @param germplasmToProcess * @return Map * @throws ApiException */ - private Map processGermplasmForDisplay(List programGermplasm, + private List processGermplasmForDisplay(List germplasmToProcess, Program program) throws ApiException { // Process the germplasm - Map programGermplasmMap = new HashMap<>(); - log.trace("processing germ for display: " + programGermplasm); - for (BrAPIGermplasm germplasm: programGermplasm) { + log.trace("processing germ for display: " + germplasmToProcess); + for (BrAPIGermplasm germplasm: germplasmToProcess) { JsonObject additionalInfo = germplasm.getAdditionalInfo(); if (additionalInfo != null && additionalInfo.has(BrAPIAdditionalInfoFields.GERMPLASM_BREEDING_METHOD_ID)) { @@ -177,15 +176,15 @@ private Map processGermplasmForDisplay(List processGermplasmForDisplay(List referenceSource.equals(reference.getReferenceSource())).findFirst().orElseThrow(() -> new IllegalStateException("No BI external reference found")); - String germplasmId = extRef.getReferenceID(); - programGermplasmMap.put(germplasmId, germplasm); + germplasm.setPedigree(gidPedigreeString); } - return programGermplasmMap; + return germplasmToProcess; } private Map getPedigreeGermplasmDbIdByBICreatedExRef(List brAPIGermplasm, Program program) throws ApiException { @@ -320,7 +315,7 @@ private Map getPedigreeGermplasmDbIdByBICreatedExRef(List getBrAPIGermplasmUsingBrAPIProgramId(GermplasmQuery List result = brAPIDAOUtil.get(api::germplasmGet, germplasmQueryParams); // TODO: Once cache is removed for this class, fix processGermplasmForDisplay to return List [BI-2906] - return new ArrayList<>(processGermplasmForDisplay(result, program).values()); + return processGermplasmForDisplay(result, program); } // TODO: hack for now, probably should update breedbase @@ -401,7 +396,7 @@ public List createBrAPIGermplasm(List postBrAPIG try { if (!postBrAPIGermplasmList.isEmpty()) { List postResponse = brAPIDAOUtil.post(postBrAPIGermplasmList, upload, api::germplasmPost, importDAO::update); - return new ArrayList<>(processGermplasmForDisplay(postResponse, program).values()); + return processGermplasmForDisplay(postResponse, program); } return new ArrayList<>(); } catch (Exception e) { @@ -415,7 +410,7 @@ public List updateBrAPIGermplasm(List putBrAPIGe try { if (!putBrAPIGermplasmList.isEmpty()) { List putResponse = putGermplasm(putBrAPIGermplasmList, api); - return new ArrayList<>(processGermplasmForDisplay(putResponse, program).values()); + return processGermplasmForDisplay(putResponse, program); } return new ArrayList<>(); } catch (Exception e) { @@ -432,18 +427,27 @@ public List getGermplasmByRawName(List germplasmNames, U .collect(Collectors.toList()); } + public BrAPIGermplasmListResponse searchGermplasmByRawName(List germplasmNames, UUID programId, GermplasmQuery germplasmQuery) throws ApiException { + Program program = new Program(programDAO.fetchOneById(programId)); + + BrAPIGermplasmSearchRequest searchRequest = new BrAPIGermplasmSearchRequest(); + searchRequest.setGermplasmNames(germplasmNames); + + return brapiGermplasmSearchReturnResponse(program, searchRequest, germplasmQuery); + } + public List brapiGermplasmSearchReturnList(Program program, - List brapiGermplasmIds) throws ApiException { - return brapiGermplasmSearchReturnResponse(program, brapiGermplasmIds, null).getResult().getData(); + BrAPIGermplasmSearchRequest searchRequest) throws ApiException { + return brapiGermplasmSearchReturnResponse(program, searchRequest, null).getResult().getData(); } public BrAPIGermplasmListResponse brapiGermplasmSearchReturnResponse(Program program, - List brapiGermplasmIds, + BrAPIGermplasmSearchRequest searchRequest, GermplasmQuery germplasmQuery) throws ApiException { GermplasmApi api = brAPIEndpointProvider.get(programDAO.getCoreClient(program.getId()), GermplasmApi.class); - BrAPIGermplasmSearchRequest brAPIGermplasmSearchRequest = buildSearchRequest(program, brapiGermplasmIds, germplasmQuery); + BrAPIGermplasmSearchRequest brAPIGermplasmSearchRequest = buildSearchRequest(program, searchRequest, germplasmQuery); BrAPIGermplasmListResponse brAPIResponse = brAPIDAOUtil.simpleSearch( @@ -451,20 +455,14 @@ public BrAPIGermplasmListResponse brapiGermplasmSearchReturnResponse(Program pro brAPIGermplasmSearchRequest ); - // TODO: Once cache is removed for this class, fix processGermplasmForDisplay to return List [BI-2906] - List processedGermplasm = - new ArrayList<>(processGermplasmForDisplay(brAPIDAOUtil.getListResult(brAPIResponse), program).values()); + List processedGermplasm = processGermplasmForDisplay(brAPIDAOUtil.getListResult(brAPIResponse), program); brAPIResponse.getResult().setData(processedGermplasm); return brAPIResponse; } - private BrAPIGermplasmSearchRequest buildSearchRequest(Program program, List brapiGermplasmIds, GermplasmQuery query) throws ApiException { - return buildSearchRequest(program, brapiGermplasmIds, query, null); - } - - private BrAPIGermplasmSearchRequest buildSearchRequest(Program program, List brapiGermplasmIds, GermplasmQuery germplasmQuery, BrAPIGermplasmSearchRequest searchRequestPassThru) throws ApiException { + private BrAPIGermplasmSearchRequest buildSearchRequest(Program program, BrAPIGermplasmSearchRequest searchRequestPassThru, GermplasmQuery germplasmQuery) throws ApiException { BrAPIGermplasmSearchRequest searchRequest; if (searchRequestPassThru == null) { @@ -475,10 +473,6 @@ private BrAPIGermplasmSearchRequest buildSearchRequest(Program program, List(brapiGermplasmIds)); - } - brAPIDAOUtil.setGenericSearchParameters(searchRequest, germplasmQuery); return searchRequest; @@ -487,7 +481,10 @@ private BrAPIGermplasmSearchRequest buildSearchRequest(Program program, List result = brapiGermplasmSearchReturnList(program, List.of(germplasmId)); + BrAPIGermplasmSearchRequest searchRequest = new BrAPIGermplasmSearchRequest(); + searchRequest.setGermplasmDbIds(List.of(germplasmId)); + + List result = brapiGermplasmSearchReturnList(program, searchRequest); if (result.size() > 1) { throw new ApiException(String.format("Multiple germplasms found for germplasm with ID: [%s]", germplasmId)); @@ -502,7 +499,10 @@ public List getGermplasmsByDBID(Collection germplasmDbId // TODO: This method is mainly used by the download experiment export tool. This method will fail until we address the parameter limit for async Germplasm requests for germplasmDbIds > 4. Return to this use case during [BI-3021] Program program = new Program(programDAO.fetchOneById(programId)); - return brapiGermplasmSearchReturnList(program, new ArrayList<>(germplasmDbIds)); + BrAPIGermplasmSearchRequest searchRequest = new BrAPIGermplasmSearchRequest(); + searchRequest.setGermplasmDbIds(new ArrayList<>(germplasmDbIds)); + + return brapiGermplasmSearchReturnList(program, searchRequest); } public List putGermplasm(List germplasmList, GermplasmApi api) throws ApiException { diff --git a/src/main/java/org/breedinginsight/brapi/v2/model/request/query/GermplasmQuery.java b/src/main/java/org/breedinginsight/brapi/v2/model/request/query/GermplasmQuery.java index 81039b7ef..2c1807490 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/model/request/query/GermplasmQuery.java +++ b/src/main/java/org/breedinginsight/brapi/v2/model/request/query/GermplasmQuery.java @@ -8,7 +8,9 @@ import org.jooq.tools.StringUtils; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Getter @Introspected @@ -67,4 +69,42 @@ public SearchRequest constructSearchRequest() { } return new SearchRequest(filters); } + + @Override + public Map getFilterValuesByBrAPIColumnName() { + Map filterValuesByBrAPIColumnName = new HashMap<>(); + + filterValuesByBrAPIColumnName.put("importEntryNumber", getImportEntryNumber()); + filterValuesByBrAPIColumnName.put("accessionNumber", getAccessionNumber()); + filterValuesByBrAPIColumnName.put("defaultDisplayName", getDefaultDisplayName()); + filterValuesByBrAPIColumnName.put("breedingMethod", getBreedingMethod()); + filterValuesByBrAPIColumnName.put("seedSource", getSeedSource()); + filterValuesByBrAPIColumnName.put("pedigree", getPedigree()); + filterValuesByBrAPIColumnName.put("femaleParentGID", getFemaleParentGID()); + filterValuesByBrAPIColumnName.put("maleParentGID", getMaleParentGID()); + filterValuesByBrAPIColumnName.put("createdDate", getCreatedDate()); + filterValuesByBrAPIColumnName.put("createdBy", getCreatedByUserName()); + filterValuesByBrAPIColumnName.put("synonyms", getSynonym()); + + return filterValuesByBrAPIColumnName; + } + + @Override + public Map getBrAPIColumnNamesByBiColumnName() { + Map brAPIColumnNamesByBiColumnName = new HashMap<>(); + + brAPIColumnNamesByBiColumnName.put("importEntryNumber", "importEntryNumber"); + brAPIColumnNamesByBiColumnName.put("accessionNumber", "accessionNumber"); + brAPIColumnNamesByBiColumnName.put("defaultDisplayName", "defaultDisplayName"); + brAPIColumnNamesByBiColumnName.put("breedingMethod", "breedingMethod"); + brAPIColumnNamesByBiColumnName.put("seedSource", "seedSource"); + brAPIColumnNamesByBiColumnName.put("pedigree", "pedigree"); + brAPIColumnNamesByBiColumnName.put("femaleParentGID", "femaleParentGID"); + brAPIColumnNamesByBiColumnName.put("maleParentGID", "maleParentGID"); + brAPIColumnNamesByBiColumnName.put("createdDate", "createdDate"); + brAPIColumnNamesByBiColumnName.put("createdBy", "createdByUserName"); + brAPIColumnNamesByBiColumnName.put("synonyms", "synonyms"); + + return brAPIColumnNamesByBiColumnName; + } } diff --git a/src/main/java/org/breedinginsight/brapi/v2/services/BrAPIGermplasmService.java b/src/main/java/org/breedinginsight/brapi/v2/services/BrAPIGermplasmService.java index c68801a8b..333d9910f 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/services/BrAPIGermplasmService.java +++ b/src/main/java/org/breedinginsight/brapi/v2/services/BrAPIGermplasmService.java @@ -4,10 +4,7 @@ import io.micronaut.context.annotation.Property; import io.micronaut.http.server.exceptions.InternalServerException; import io.micronaut.http.server.types.files.StreamedFile; -import org.apache.commons.lang3.tuple.Pair; -import org.brapi.client.v2.ApiResponse; import org.brapi.client.v2.model.exceptions.ApiException; -import org.brapi.v2.model.BrAPIAcceptedSearchResponse; import org.brapi.v2.model.BrAPIExternalReference; import lombok.extern.slf4j.Slf4j; import org.brapi.v2.model.core.request.BrAPIListNewRequest; @@ -15,10 +12,10 @@ import org.brapi.v2.model.core.response.BrAPIListsSingleResponse; import org.brapi.v2.model.germ.BrAPIGermplasm; import org.brapi.v2.model.germ.BrAPIGermplasmSynonyms; -import org.brapi.v2.model.germ.request.BrAPIGermplasmSearchRequest; import org.brapi.v2.model.germ.response.BrAPIGermplasmListResponse; import org.breedinginsight.brapi.v2.constants.BrAPIAdditionalInfoFields; import org.breedinginsight.brapi.v2.dao.BrAPIListDAO; +import org.breedinginsight.brapi.v2.model.request.query.GermplasmQuery; import org.breedinginsight.brapps.importer.model.exports.FileType; import org.breedinginsight.model.Column; import org.breedinginsight.model.DownloadFile; @@ -169,19 +166,27 @@ public List> processListData(List germplasm, return processedData; } - public List getGermplasmByList(UUID programId, String listDbId) throws ApiException { + public BrAPIGermplasmListResponse getGermplasmByList(UUID programId, GermplasmQuery germplasmQuery) throws ApiException, DoesNotExistException { // get list germplasm names - BrAPIListsSingleResponse listResponse = brAPIListDAO.getListById(listDbId, programId); + BrAPIListsSingleResponse listResponse = brAPIListDAO.getListById(germplasmQuery.getListDbId(), programId); if(Objects.nonNull(listResponse) && Objects.nonNull(listResponse.getResult())) { // get the list ID stored in the list external references UUID listId = getGermplasmListId(listResponse.getResult()); - // get list BrAPI germplasm variables - List germplasmNames = listResponse.getResult().getData(); - List germplasm = germplasmDAO.getGermplasmByRawName(germplasmNames, programId); + // get list BrAPI germplasm variables. + // The order of these germplasm names are the exact order/entry number in the list that was imported. + List germplasmNamesFromBrapiList = listResponse.getResult().getData(); + + BrAPIGermplasmListResponse germplasmSearchResponse = germplasmDAO.searchGermplasmByRawName(germplasmNamesFromBrapiList, programId, germplasmQuery); + + List germplasm = germplasmSearchResponse.getResult().getData(); Map germplasmByGid = new HashMap<>(); + if (germplasm == null || germplasm.isEmpty()) { + return germplasmSearchResponse; + } + for (BrAPIGermplasm g : germplasm) { // set the list ID in the germplasm additional info g.putAdditionalInfoItem(BrAPIAdditionalInfoFields.GERMPLASM_LIST_ID, listId); @@ -190,23 +195,68 @@ public List getGermplasmByList(UUID programId, String listDbId) } // Extract gids from list names - List gids = germplasmNames.stream().map(Utilities::extractGid).collect(Collectors.toList()); + List gids = germplasmNamesFromBrapiList.stream().map(Utilities::extractGid).collect(Collectors.toList()); // Build list from BrAPI list that preserves ordering and duplicates and assigns sequential entry numbers. - List germplasmList = new ArrayList<>(); - int entryNumber = 0; + List orderByEntryNumber = new ArrayList<>(); + + Program program = programService.getById(programId).orElseThrow(() -> new DoesNotExistException("Could not find program: " + programId)); + for (String gid : gids) { - ++entryNumber; - BrAPIGermplasm listEntry = cloneBrAPIGermplasm(germplasmByGid.get(gid)); + BrAPIGermplasm germ = germplasmByGid.get(gid); + + if (germ == null) { + continue; + } + + // We can't simply use germ.getGermplasmName here because the BrAPIGermplasmDAO code strips the key and gid via BrAPIGermplasmDAO.processGermplasmForDisplay() + // Rebuild the unique name so we can get the entry number via the order on the germplasmNamesFromBrapiList list which was derived from the list response. + String fullGermplasmName = Utilities.appendProgramKey(germ.getDefaultDisplayName(), program.getKey(), germ.getAccessionNumber()); + + BrAPIGermplasm listEntry = cloneBrAPIGermplasm(germ); + + int entryNumber = germplasmNamesFromBrapiList.indexOf(fullGermplasmName) + 1; + // Set entry number. listEntry.putAdditionalInfoItem(BrAPIAdditionalInfoFields.GERMPLASM_IMPORT_ENTRY_NUMBER, entryNumber); - germplasmList.add(listEntry); + germ.putAdditionalInfoItem(BrAPIAdditionalInfoFields.GERMPLASM_IMPORT_ENTRY_NUMBER, entryNumber); + orderByEntryNumber.add(listEntry); + } + + if (germplasmQuery.getSortField() == null || germplasmQuery.getSortField().equals("importEntryNumber")) { + // This should order by entry number when germplasm list details page displays + germplasmSearchResponse.getResult().setData(orderByEntryNumber); + } else { + // In all other cases it will order by the other selected data + germplasmSearchResponse.getResult().setData(germplasm); } - return germplasmList; + return germplasmSearchResponse; } else throw new ApiException(); } +// private BrAPIGermplasmListResponse orderByListImportEntryNumber() { +// for (String gid : gids) { +// BrAPIGermplasm germ = germplasmByGid.get(gid); +// +// if (germ == null) { +// continue; +// } +// +// // We can't simply use germ.getGermplasmName here because the BrAPIGermplasmDAO code strips the key and gid via BrAPIGermplasmDAO.processGermplasmForDisplay() +// // Rebuild the unique name so we can get the entry number via the order on the germplasmNamesFromBrapiList list which was derived from the list response. +// String fullGermplasmName = Utilities.appendProgramKey(germ.getDefaultDisplayName(), program.getKey(), germ.getAccessionNumber()); +// +// BrAPIGermplasm listEntry = cloneBrAPIGermplasm(germ); +// +// int entryNumber = germplasmNamesFromBrapiList.indexOf(fullGermplasmName) + 1; +// +// // Set entry number. +// listEntry.putAdditionalInfoItem(BrAPIAdditionalInfoFields.GERMPLASM_IMPORT_ENTRY_NUMBER, entryNumber); +// orderByEntryNumber.add(listEntry); +// } +// } + private BrAPIGermplasm cloneBrAPIGermplasm(BrAPIGermplasm germplasm) { // Serialize then deserialize to deep copy. return (BrAPIGermplasm) gson.fromJson(gson.toJson(germplasm), BrAPIGermplasm.class); @@ -355,4 +405,11 @@ public List getGermplasmByDisplayName(List germplasmDisp } return matchingGermplasm; } + + public BrAPIGermplasmListResponse searchGermplasm(UUID programId, + GermplasmQuery germplasmQuery) throws ApiException, DoesNotExistException { + Program program = programService.getById(programId).orElseThrow(() -> new DoesNotExistException("Could not find program: " + programId)); + + return germplasmDAO.brapiGermplasmSearchReturnResponse(program, null, germplasmQuery); + } } diff --git a/src/test/java/org/breedinginsight/brapi/v2/GermplasmControllerIntegrationTest.java b/src/test/java/org/breedinginsight/brapi/v2/GermplasmControllerIntegrationTest.java index 2a0ad750f..fd6e41c50 100644 --- a/src/test/java/org/breedinginsight/brapi/v2/GermplasmControllerIntegrationTest.java +++ b/src/test/java/org/breedinginsight/brapi/v2/GermplasmControllerIntegrationTest.java @@ -396,6 +396,7 @@ public void searchGermplasmOneMatchOneNoMatch() { @Test @SneakyThrows public void filterGermplasmCreatedDateSuccess() { + // TODO: This test is going to continue to fail until date data is migrated to the proper format [BI-3057] // Case 1: createdDate="/". // If createdDate is filtered in the default format, "dd/MM/yyyy HH:mm:ss", "/" will match all, test fails. // If createdDate is filtered after formatting with "yyyy-MM-dd", "/" will match none, test succeeds.