diff --git a/src/main/java/org/breedinginsight/brapi/v2/BrAPIPedigreeController.java b/src/main/java/org/breedinginsight/brapi/v2/BrAPIPedigreeController.java new file mode 100644 index 000000000..51799b346 --- /dev/null +++ b/src/main/java/org/breedinginsight/brapi/v2/BrAPIPedigreeController.java @@ -0,0 +1,137 @@ +/* + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.breedinginsight.brapi.v2; + +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.annotation.*; +import io.micronaut.security.annotation.Secured; +import io.micronaut.security.rules.SecurityRule; +import lombok.extern.slf4j.Slf4j; +import org.brapi.client.v2.model.exceptions.ApiException; +import org.brapi.v2.model.BrAPIIndexPagination; +import org.brapi.v2.model.BrAPIMetadata; +import org.brapi.v2.model.germ.BrAPIPedigreeNode; +import org.brapi.v2.model.germ.response.BrAPIPedigreeListResponse; +import org.brapi.v2.model.germ.response.BrAPIPedigreeListResponseResult; +import org.breedinginsight.api.auth.ProgramSecured; +import org.breedinginsight.api.auth.ProgramSecuredRoleGroup; +import org.breedinginsight.brapi.v1.controller.BrapiVersion; +import org.breedinginsight.brapi.v2.dao.BrAPIPedigreeDAO; +import org.breedinginsight.model.Program; +import org.breedinginsight.services.ProgramService; +import org.breedinginsight.utilities.Utilities; + +import javax.annotation.Nullable; +import javax.inject.Inject; +import java.util.*; + +@Slf4j +@Controller("/${micronaut.bi.api.version}/programs/{programId}" + BrapiVersion.BRAPI_V2) +@Secured(SecurityRule.IS_AUTHENTICATED) +public class BrAPIPedigreeController { + private final BrAPIPedigreeDAO pedigreeDAO; + + private final ProgramService programService; + + @Inject + public BrAPIPedigreeController(BrAPIPedigreeDAO pedigreeDAO, + ProgramService programService) { + this.pedigreeDAO = pedigreeDAO; + this.programService = programService; + } + + @Get("/pedigree") + @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL}) + public HttpResponse pedigreeGet(@PathVariable("programId") UUID programId, + @Nullable @QueryValue("accessionNumber") String accessionNumber, + @Nullable @QueryValue("collection") String collection, + @Nullable @QueryValue("familyCode") String familyCode, + @Nullable @QueryValue("binomialName") String binomialName, + @Nullable @QueryValue("genus") Boolean genus, + @Nullable @QueryValue("species") String species, + @Nullable @QueryValue("synonym") String synonym, + @Nullable @QueryValue("includeParents") Boolean includeParents, + @Nullable @QueryValue("includeSiblings") Boolean includeSiblings, + @Nullable @QueryValue("includeProgeny") Boolean includeProgeny, + @Nullable @QueryValue("includeFullTree") Boolean includeFullTree, + @Nullable @QueryValue("pedigreeDepth") Integer pedigreeDepth, + @Nullable @QueryValue("progenyDepth") Integer progenyDepth, + @Nullable @QueryValue("commonCropName") String commonCropName, + @Nullable @QueryValue("programDbId") String programDbId, + @Nullable @QueryValue("trialDbId") String trialDbId, + @Nullable @QueryValue("studyDbId") String studyDbId, + @Nullable @QueryValue("germplasmDbId") String germplasmDbId, + @Nullable @QueryValue("germplasmName") String germplasmName, + @Nullable @QueryValue("germplasmPUI") String germplasmPUI, + @Nullable @QueryValue("externalReferenceId") String externalReferenceId, + @Nullable @QueryValue("externalReferenceSource") String externalReferenceSource, + @Nullable @QueryValue("page") Integer page, + @Nullable @QueryValue("pageSize") Integer pageSize) { + + log.debug("pedigreeGet: fetching pedigree by filters"); + + Optional program = programService.getById(programId); + if(program.isEmpty()) { + log.warn("Program id: " + programId + " not found"); + return HttpResponse.notFound(); + } + + try { + List pedigree = pedigreeDAO.getPedigree( + program.get(), + Optional.ofNullable(includeParents), + Optional.ofNullable(includeSiblings), + Optional.ofNullable(includeProgeny), + Optional.ofNullable(includeFullTree), + Optional.ofNullable(pedigreeDepth), + Optional.ofNullable(progenyDepth), + Optional.ofNullable(germplasmName)); + + return HttpResponse.ok( + new BrAPIPedigreeListResponse() + .metadata(new BrAPIMetadata().pagination(new BrAPIIndexPagination().currentPage(0) + .totalPages(1) + .pageSize(pedigree.size()) + .totalCount(pedigree.size()))) + .result(new BrAPIPedigreeListResponseResult().data(pedigree)) + ); + } catch (ApiException e) { + log.error(Utilities.generateApiExceptionLogMessage(e), e); + return HttpResponse.status(HttpStatus.INTERNAL_SERVER_ERROR, "error fetching pedigree"); + } + } + + @Post("/pedigree") + @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL}) + public HttpResponse pedigreePost(@PathVariable("programId") UUID programId, @Body List body) { + //DO NOT IMPLEMENT - Users are only able to create pedigree via the DeltaBreed UI + return HttpResponse.notFound(); + } + + @Put("/pedigree") + @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL}) + public HttpResponse pedigreePut(@PathVariable("programId") UUID programId, @Body Map body) { + //DO NOT IMPLEMENT - Users aren't yet able to update observation units + return HttpResponse.notFound(); + } + + // TODO: search and retrieve endpoints + // already have some work done, search call in BrAPIPedigreeDAO + +} diff --git a/src/main/java/org/breedinginsight/brapi/v2/BrAPIV2Controller.java b/src/main/java/org/breedinginsight/brapi/v2/BrAPIV2Controller.java index d6e4dde43..b5249516e 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/BrAPIV2Controller.java +++ b/src/main/java/org/breedinginsight/brapi/v2/BrAPIV2Controller.java @@ -43,6 +43,7 @@ import java.io.IOException; import java.util.List; import java.util.UUID; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @Slf4j @@ -147,7 +148,7 @@ public BrAPIServerInfoResponse programServerinfo(@PathVariable("programId") UUID .build(); for(BrAPIService service : programServices) { - service.setService(programBrAPIBase + service.getService()); + service.setService(service.getService()); } BrAPIServerInfo programServerInfo = new BrAPIServerInfo(); @@ -230,7 +231,10 @@ private HttpResponse executeRequest(String path, UUID programId, HttpReq } private HttpResponse makeCall(Request brapiRequest) { - OkHttpClient client = new OkHttpClient(); + // TODO: use config parameter for timeout + OkHttpClient client = new OkHttpClient.Builder() + .readTimeout(5, TimeUnit.MINUTES) + .build(); try (Response brapiResponse = client.newCall(brapiRequest).execute()) { if(brapiResponse.isSuccessful()) { try(ResponseBody body = brapiResponse.body()) { diff --git a/src/main/java/org/breedinginsight/brapi/v2/dao/BrAPIPedigreeDAO.java b/src/main/java/org/breedinginsight/brapi/v2/dao/BrAPIPedigreeDAO.java new file mode 100644 index 000000000..a00957f2c --- /dev/null +++ b/src/main/java/org/breedinginsight/brapi/v2/dao/BrAPIPedigreeDAO.java @@ -0,0 +1,215 @@ +/* + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.breedinginsight.brapi.v2.dao; + +import io.micronaut.context.annotation.Property; +import io.micronaut.http.server.exceptions.InternalServerException; +import lombok.extern.slf4j.Slf4j; +import org.brapi.client.v2.ApiResponse; +import org.brapi.client.v2.model.exceptions.ApiException; +import org.brapi.client.v2.model.queryParams.germplasm.PedigreeQueryParams; +import org.brapi.client.v2.modules.germplasm.PedigreeApi; +import org.brapi.v2.model.germ.BrAPIPedigreeNode; +import org.brapi.v2.model.germ.request.BrAPIPedigreeSearchRequest; +import org.brapi.v2.model.germ.response.BrAPIPedigreeListResponse; +import org.breedinginsight.brapps.importer.services.ExternalReferenceSource; +import org.breedinginsight.daos.ProgramDAO; +import org.breedinginsight.model.Program; +import org.breedinginsight.services.brapi.BrAPIEndpointProvider; +import org.breedinginsight.utilities.BrAPIDAOUtil; +import org.breedinginsight.utilities.Utilities; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.*; + +@Singleton +@Slf4j +public class BrAPIPedigreeDAO { + + private ProgramDAO programDAO; + private final BrAPIDAOUtil brAPIDAOUtil; + private final BrAPIEndpointProvider brAPIEndpointProvider; + private final String referenceSource; + + @Inject + public BrAPIPedigreeDAO(ProgramDAO programDAO, BrAPIDAOUtil brAPIDAOUtil, + BrAPIEndpointProvider brAPIEndpointProvider, + @Property(name = "brapi.server.reference-source") String referenceSource) { + this.programDAO = programDAO; + this.brAPIDAOUtil = brAPIDAOUtil; + this.brAPIEndpointProvider = brAPIEndpointProvider; + this.referenceSource = referenceSource; + } + + /** + * Retrieves the pedigree of a given program and optional filters. Used by Helium. TODO: Add rest of parameters + * + * @param program The program for which the pedigree is requested. + * @param includeParents Flag to indicate whether to include parent nodes in the pedigree. (optional) + * @param includeSiblings Flag to indicate whether to include sibling nodes in the pedigree. (optional) + * @param includeProgeny Flag to indicate whether to include progeny nodes in the pedigree. (optional) + * @param includeFullTree Flag to indicate whether to include the full pedigree tree or only immediate ancestors and descendants. (optional) + * @param pedigreeDepth The maximum depth of ancestors and descendants to include in the pedigree. (optional) + * @param progenyDepth The maximum depth of progeny to include in the pedigree. (optional) + * @param germplasmName The name of the germplasm to which the pedigree is limited. (optional) + * @return A list of pedigree nodes representing the pedigree of the program. + * @throws ApiException If an error occurs while making the BrAPI call. + */ + public List getPedigree( + Program program, + Optional includeParents, + Optional includeSiblings, + Optional includeProgeny, + Optional includeFullTree, + Optional pedigreeDepth, + Optional progenyDepth, + Optional germplasmName + ) throws ApiException { + + PedigreeQueryParams pedigreeRequest = new PedigreeQueryParams(); + + // TODO: Issue with BrAPI server programDbId filtering, think germplasm are linked to program through observation + // units and doesn't work if don't have any loaded + // use external refs instead for now + //pedigreeSearchRequest.programDbIds(List.of(program.getBrapiProgram().getProgramDbId())); + + String extRefId = program.getId().toString(); + String extRefSrc = Utilities.generateReferenceSource(referenceSource, ExternalReferenceSource.PROGRAMS); + pedigreeRequest.externalReferenceId(extRefId); + pedigreeRequest.externalReferenceSource(extRefSrc); + + includeParents.ifPresent(pedigreeRequest::includeParents); + includeSiblings.ifPresent(pedigreeRequest::includeSiblings); + includeProgeny.ifPresent(pedigreeRequest::includeProgeny); + includeFullTree.ifPresent(pedigreeRequest::includeFullTree); + pedigreeDepth.ifPresent(pedigreeRequest::pedigreeDepth); + progenyDepth.ifPresent(pedigreeRequest::progenyDepth); + germplasmName.ifPresent(pedigreeRequest::germplasmName); + // TODO: other parameters + + // TODO: write utility to do paging instead of hardcoding + pedigreeRequest.pageSize(100000); + + ApiResponse brapiPedigree; + try { + brapiPedigree = brAPIEndpointProvider + .get(programDAO.getCoreClient(program.getId()), PedigreeApi.class) + .pedigreeGet(pedigreeRequest); + } catch (ApiException e) { + log.warn(Utilities.generateApiExceptionLogMessage(e)); + throw new InternalServerException("Error making BrAPI call", e); + } + + List pedigreeNodes = brapiPedigree.getBody().getResult().getData(); + // TODO: once Helium is constructing nodes from DbId we can strip program keys but won't in the mean time + //stripProgramKeys(pedigreeNodes, program.getKey()); + return pedigreeNodes; + } + + /** + * Searches for pedigree nodes based on the given parameters. Not used by Helium but keeping commented out for + * now in case we want to implement the search endpoints in the future, work here has already been started to + * support that. + * + * TODO: Add rest of parameters + * + * @param program The program to search for pedigree nodes. + * @param includeParents Optional boolean to include parents in the search. + * @param includeSiblings Optional boolean to include siblings in the search. + * @param includeProgeny Optional boolean to include progeny in the search. + * @param includeFullTree Optional boolean to include the full pedigree tree in the search. + * @param pedigreeDepth Optional integer for the maximum depth of the pedigree tree. + * @param progenyDepth Optional integer for the maximum depth of the progeny tree. + * @param germplasmName Optional String to filter the search by germplasm name. + * @return A List of BrAPIPedigreeNode objects that match the search criteria. + * @throws ApiException If an error occurs while searching for pedigree nodes. + */ + /* + public List searchPedigree(Program program, + Optional includeParents, + Optional includeSiblings, + Optional includeProgeny, + Optional includeFullTree, + Optional pedigreeDepth, + Optional progenyDepth, + Optional germplasmName + ) throws ApiException { + + BrAPIPedigreeSearchRequest pedigreeSearchRequest = new BrAPIPedigreeSearchRequest(); + // TODO: Issue with BrAPI server programDbId filtering, think germplasm are linked to program through observation + // units and doesn't work if don't have any loaded + // use external refs instead for now + //pedigreeSearchRequest.programDbIds(List.of(program.getBrapiProgram().getProgramDbId())); + + // Just use program UUID, shouldn't have any collisions and don't want to get all the germplasm if we were also + // using source because search is OR rather than AND + String extRefId = program.getId().toString(); + pedigreeSearchRequest.addExternalReferenceIdsItem(extRefId); + + includeParents.ifPresent(pedigreeSearchRequest::includeParents); + includeSiblings.ifPresent(pedigreeSearchRequest::includeSiblings); + includeProgeny.ifPresent(pedigreeSearchRequest::includeProgeny); + includeFullTree.ifPresent(pedigreeSearchRequest::includeFullTree); + pedigreeDepth.ifPresent(pedigreeSearchRequest::setPedigreeDepth); + progenyDepth.ifPresent(pedigreeSearchRequest::setProgenyDepth); + germplasmName.ifPresent(pedigreeSearchRequest::addGermplasmNamesItem); + // TODO: other parameters + + PedigreeApi api = brAPIEndpointProvider.get(programDAO.getCoreClient(program.getId()), PedigreeApi.class); + + List pedigreeNodes = brAPIDAOUtil.search( + api::searchPedigreePost, + api::searchPedigreeSearchResultsDbIdGet, + pedigreeSearchRequest + ); + + // TODO: once Helium is constructing nodes from DbId we can strip program keys but won't in the mean time + //stripProgramKeys(pedigreeNodes, program.getKey()); + + return pedigreeNodes; + } + */ + + /** + * Removes the program key from the germplasm names in the list of pedigree nodes. Not used currently but will be in + * future if we decide to strip the program keys when Helium has been updated to use the germplasmDbId for uniqueness + * rather than germplasmName. + * + * @param pedigreeNodes The list of pedigree nodes. + * @param programKey The program key to be removed. + */ + private void stripProgramKeys(List pedigreeNodes, String programKey) { + pedigreeNodes.forEach(node -> { + node.setGermplasmName(Utilities.removeProgramKeyAnyAccession(node.getGermplasmName(), programKey)); + // TODO: pedigree stripping not working right + //node.setPedigreeString(Utilities.removeProgramKeyAnyAccession(node.getPedigreeString(), programKey)); + if (node.getParents() != null) { + node.getParents().forEach(parent -> { + parent.setGermplasmName(Utilities.removeProgramKeyAnyAccession(parent.getGermplasmName(), programKey)); + }); + } + if (node.getProgeny() != null) { + node.getProgeny().forEach(progeny -> { + progeny.setGermplasmName(Utilities.removeProgramKeyAnyAccession(progeny.getGermplasmName(), programKey)); + }); + } + }); + } + +} \ No newline at end of file diff --git a/src/main/java/org/breedinginsight/utilities/Utilities.java b/src/main/java/org/breedinginsight/utilities/Utilities.java index 2ff850d55..2231f3a16 100644 --- a/src/main/java/org/breedinginsight/utilities/Utilities.java +++ b/src/main/java/org/breedinginsight/utilities/Utilities.java @@ -86,6 +86,16 @@ public static String removeUnknownProgramKey(String original) { return original.replaceAll("\\[.*\\]", "").trim(); } + /** + * Removes the program key from a string with any accession number. + * + * @param str The string to remove the program key from + * @param programKey The program key to remove + * @return The modified string + */ + public static String removeProgramKeyAnyAccession(String str, String programKey) { + return str.replaceAll("\\[" + programKey + "-.*\\]", "").trim(); + } /** * Remove program key from a string. Returns a new value instead of altering original string.