From 65675f713e7d31d7194a7dcc90b50ce287580b63 Mon Sep 17 00:00:00 2001 From: mlm483 <128052931+mlm483@users.noreply.github.com> Date: Sun, 16 Jul 2023 16:13:37 -0400 Subject: [PATCH 1/6] [BI-1720] - added BrAPI Study specific code --- .../brapi/v2/StudyController.java | 107 ++++++++++ .../brapi/v2/dao/BrAPIStudyDAO.java | 198 ++++++++++++++++++ .../v2/model/request/query/StudyQuery.java | 119 +++++++++++ .../brapi/v2/services/BrAPIStudyService.java | 59 ++++++ .../response/mappers/StudyQueryMapper.java | 54 +++++ 5 files changed, 537 insertions(+) create mode 100644 src/main/java/org/breedinginsight/brapi/v2/StudyController.java create mode 100644 src/main/java/org/breedinginsight/brapi/v2/dao/BrAPIStudyDAO.java create mode 100644 src/main/java/org/breedinginsight/brapi/v2/model/request/query/StudyQuery.java create mode 100644 src/main/java/org/breedinginsight/brapi/v2/services/BrAPIStudyService.java create mode 100644 src/main/java/org/breedinginsight/utilities/response/mappers/StudyQueryMapper.java diff --git a/src/main/java/org/breedinginsight/brapi/v2/StudyController.java b/src/main/java/org/breedinginsight/brapi/v2/StudyController.java new file mode 100644 index 000000000..69e5697a5 --- /dev/null +++ b/src/main/java/org/breedinginsight/brapi/v2/StudyController.java @@ -0,0 +1,107 @@ +package org.breedinginsight.brapi.v2; + +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.*; +import io.micronaut.http.server.exceptions.InternalServerException; +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.core.BrAPIStudy; +import org.breedinginsight.api.auth.ProgramSecured; +import org.breedinginsight.api.auth.ProgramSecuredRoleGroup; +import org.breedinginsight.api.model.v1.request.query.SearchRequest; +import org.breedinginsight.api.model.v1.response.DataResponse; +import org.breedinginsight.api.model.v1.response.Response; +import org.breedinginsight.api.model.v1.validators.QueryValid; +import org.breedinginsight.brapi.v1.controller.BrapiVersion; +import org.breedinginsight.brapi.v1.model.request.query.BrapiQuery; +import org.breedinginsight.brapi.v2.model.request.query.StudyQuery; +import org.breedinginsight.brapi.v2.services.BrAPIStudyService; +import org.breedinginsight.services.exceptions.DoesNotExistException; +import org.breedinginsight.utilities.response.ResponseUtils; +import org.breedinginsight.utilities.response.mappers.StudyQueryMapper; + +import javax.inject.Inject; +import javax.validation.Valid; +import java.util.List; +import java.util.UUID; + +@Slf4j +@Controller("/${micronaut.bi.api.version}") +@Secured(SecurityRule.IS_AUTHENTICATED) +public class StudyController { + + private final BrAPIStudyService studyService; + private final StudyQueryMapper studyQueryMapper; + + + @Inject + public StudyController(BrAPIStudyService studyService, StudyQueryMapper studyQueryMapper) { + this.studyService = studyService; + this.studyQueryMapper = studyQueryMapper; + } + + @Post("/programs/{programId}" + BrapiVersion.BRAPI_V2 + "/search/studies{?queryParams*}") + @Produces(MediaType.APPLICATION_JSON) + @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL}) + public HttpResponse>>> searchStudy( + @PathVariable("programId") UUID programId, + @QueryValue @QueryValid(using = StudyQueryMapper.class) @Valid BrapiQuery queryParams, + @Body @Valid StudyQuery body) { + try { + log.debug("fetching studies for program: " + programId); + List studies = studyService.getStudies(programId); + queryParams.setSortField(studyQueryMapper.getDefaultSortField()); + queryParams.setSortOrder(studyQueryMapper.getDefaultSortOrder()); + // Filter the response. + return ResponseUtils.getBrapiQueryResponse(studies, studyQueryMapper, queryParams, body.constructSearchRequest()); + } catch (ApiException e) { + log.info(e.getMessage(), e); + return HttpResponse.status(HttpStatus.INTERNAL_SERVER_ERROR, "Error retrieving study"); + } + } + + @Get("/programs/{programId}" + BrapiVersion.BRAPI_V2 + "/studies{?queryParams*}") + @Produces(MediaType.APPLICATION_JSON) + @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL}) + public HttpResponse>>> getStudy( + @PathVariable("programId") UUID programId, + @QueryValue @QueryValid(using = StudyQueryMapper.class) @Valid StudyQuery queryParams) { + try { + log.debug("fetching studies for program: " + programId); + + List study = studyService.getStudies(programId); + SearchRequest searchRequest = queryParams.constructSearchRequest(); + return ResponseUtils.getBrapiQueryResponse(study, studyQueryMapper, queryParams, searchRequest); + } catch (ApiException e) { + log.info(e.getMessage(), e); + return HttpResponse.status(HttpStatus.INTERNAL_SERVER_ERROR, "Error retrieving study"); + } catch (IllegalArgumentException e) { + log.info(e.getMessage(), e); + return HttpResponse.status(HttpStatus.UNPROCESSABLE_ENTITY, "Error parsing requested date format"); + } + } + + @Get("/programs/{programId}" + BrapiVersion.BRAPI_V2 + "/studies/{studyId}") + @Produces(MediaType.APPLICATION_JSON) + @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL}) + public HttpResponse> getSingleStudy( + @PathVariable("programId") UUID programId, + @PathVariable("studyId") String studyId) { + try { + log.debug("fetching study id:" + studyId +" for program: " + programId); + Response response = new Response(studyService.getStudyByUUID(programId, studyId)); + return HttpResponse.ok(response); + } catch (InternalServerException e) { + log.info(e.getMessage(), e); + return HttpResponse.status(HttpStatus.INTERNAL_SERVER_ERROR, "Error retrieving study"); + } catch (DoesNotExistException e) { + log.info(e.getMessage(), e); + return HttpResponse.status(HttpStatus.NOT_FOUND, "Study not found"); + } + } + +} diff --git a/src/main/java/org/breedinginsight/brapi/v2/dao/BrAPIStudyDAO.java b/src/main/java/org/breedinginsight/brapi/v2/dao/BrAPIStudyDAO.java new file mode 100644 index 000000000..53ec5d5b0 --- /dev/null +++ b/src/main/java/org/breedinginsight/brapi/v2/dao/BrAPIStudyDAO.java @@ -0,0 +1,198 @@ +/* + * 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 com.google.gson.JsonObject; +import io.micronaut.context.annotation.Context; +import io.micronaut.context.annotation.Property; +import io.micronaut.http.server.exceptions.InternalServerException; +import io.micronaut.scheduling.annotation.Scheduled; +import lombok.extern.slf4j.Slf4j; +import org.brapi.client.v2.model.exceptions.ApiException; +import org.brapi.client.v2.modules.core.StudiesApi; +import org.brapi.v2.model.BrAPIExternalReference; +import org.brapi.v2.model.core.BrAPIStudy; +import org.brapi.v2.model.core.request.BrAPIStudySearchRequest; +import org.breedinginsight.brapps.importer.services.ExternalReferenceSource; +import org.breedinginsight.daos.ProgramDAO; +import org.breedinginsight.daos.cache.ProgramCache; +import org.breedinginsight.daos.cache.ProgramCacheProvider; +import org.breedinginsight.model.Program; +import org.breedinginsight.services.brapi.BrAPIEndpointProvider; +import org.breedinginsight.services.exceptions.DoesNotExistException; +import org.breedinginsight.utilities.BrAPIDAOUtil; +import org.breedinginsight.utilities.Utilities; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Singleton +@Context +public class BrAPIStudyDAO { + + private final ProgramDAO programDAO; + private final BrAPIDAOUtil brAPIDAOUtil; + + @Property(name = "brapi.server.reference-source") + private String referenceSource; + + @Property(name = "micronaut.bi.api.run-scheduled-tasks") + private boolean runScheduledTasks; + + private final ProgramCache programStudyCache; + + private final BrAPIEndpointProvider brAPIEndpointProvider; + + @Inject + public BrAPIStudyDAO(ProgramDAO programDAO, BrAPIDAOUtil brAPIDAOUtil, ProgramCacheProvider programCacheProvider, BrAPIEndpointProvider brAPIEndpointProvider) { + this.programDAO = programDAO; + this.brAPIDAOUtil = brAPIDAOUtil; + this.programStudyCache = programCacheProvider.getProgramCache(this::fetchProgramStudy, BrAPIStudy.class); + this.brAPIEndpointProvider = brAPIEndpointProvider; + } + + @Scheduled(initialDelay = "2s") + public void setup() { + if(!runScheduledTasks) { + return; + } + // Populate study cache for all programs on startup + log.debug("populating study cache"); + List programs = programDAO.getActive(); + if(programs != null) { + programStudyCache.populate(programs.stream().map(Program::getId).collect(Collectors.toList())); + } + } + + /** + * Fetch the study for this program, and process it to remove storage specific values + * @param programId + * @return this program's study + * @throws ApiException + */ + public List getStudies(UUID programId) throws ApiException { + return new ArrayList<>(programStudyCache.get(programId).values()); + } + + /** + * Fetch formatted study for this program + * @param programId + * @return Map + * @throws ApiException + */ + private Map fetchProgramStudy(UUID programId) throws ApiException { + StudiesApi api = brAPIEndpointProvider.get(programDAO.getCoreClient(programId), StudiesApi.class); + // Get the program key + List programs = programDAO.get(programId); + if (programs.size() != 1) { + throw new InternalServerException("Program was not found for given key"); + } + Program program = programs.get(0); + + // Set query params and make call + BrAPIStudySearchRequest studySearch = new BrAPIStudySearchRequest(); + studySearch.externalReferenceIDs(List.of(programId.toString())); + studySearch.externalReferenceSources(List.of(Utilities.generateReferenceSource(referenceSource, ExternalReferenceSource.PROGRAMS))); + return processStudyForDisplay(brAPIDAOUtil.search( + api::searchStudiesPost, + api::searchStudiesSearchResultsDbIdGet, + studySearch + ), program.getKey()); + } + + /** + * Process study into a format for display + * @param programStudy + * @return Map + * @throws ApiException + */ + private Map processStudyForDisplay(List programStudy, String programKey) { + // Process the study + Map programStudyMap = new HashMap<>(); + log.trace("processing germ for display: " + programStudy); + Map programStudyByFullName = new HashMap<>(); + for (BrAPIStudy study: programStudy) { + programStudyByFullName.put(study.getStudyName(), study); + // Remove program key from studyName, trialName and locationName. + if (study.getStudyName() != null) { + // Study name is appended with program key and experiment sequence number, need to remove. + study.setStudyName(Utilities.removeProgramKeyAndUnknownAdditionalData(study.getStudyName(), programKey)); + } + if (study.getTrialName() != null) { + study.setTrialName(Utilities.removeProgramKey(study.getTrialName(), programKey)); + } + if (study.getLocationName() != null) { + study.setLocationName(Utilities.removeProgramKey(study.getLocationName(), programKey)); + } + } + + String refSource = Utilities.generateReferenceSource(referenceSource, ExternalReferenceSource.STUDIES); + // Add to map. + for (BrAPIStudy study: programStudy) { + JsonObject additionalInfo = study.getAdditionalInfo(); + if(additionalInfo == null) { + additionalInfo = new JsonObject(); + study.setAdditionalInfo(additionalInfo); + } + + BrAPIExternalReference extRef = study.getExternalReferences().stream() + .filter(reference -> reference.getReferenceSource().equals(refSource)) + .findFirst().orElseThrow(() -> new IllegalStateException("No BI external reference found")); + String studyId = extRef.getReferenceID(); + programStudyMap.put(studyId, study); + } + + return programStudyMap; + } + + public BrAPIStudy getStudyByUUID(String studyId, UUID programId) throws ApiException, DoesNotExistException { + Map cache = programStudyCache.get(programId); + BrAPIStudy study = null; + if (cache != null) { + study = cache.get(studyId); + } + if (study == null) { + throw new DoesNotExistException("UUID for this study does not exist"); + } + return study; + } + + public Optional getStudyByDBID(String studyDbId, UUID programId) throws ApiException { + Map cache = programStudyCache.get(programId); + //key is UUID, want to filter by DBID + BrAPIStudy study = null; + if (cache != null) { + study = cache.values().stream().filter(x -> x.getStudyDbId().equals(studyDbId)).collect(Collectors.toList()).get(0); + } + return Optional.ofNullable(study); + } + + public List getStudiesByDBID(Collection studyDbIds, UUID programId) throws ApiException { + Map cache = programStudyCache.get(programId); + //key is UUID, want to filter by DBID + List studies = new ArrayList<>(); + if (cache != null) { + studies = cache.values().stream().filter(x -> studyDbIds.contains(x.getStudyDbId())).collect(Collectors.toList()); + } + return studies; + } + +} diff --git a/src/main/java/org/breedinginsight/brapi/v2/model/request/query/StudyQuery.java b/src/main/java/org/breedinginsight/brapi/v2/model/request/query/StudyQuery.java new file mode 100644 index 000000000..57c079d91 --- /dev/null +++ b/src/main/java/org/breedinginsight/brapi/v2/model/request/query/StudyQuery.java @@ -0,0 +1,119 @@ +package org.breedinginsight.brapi.v2.model.request.query; + +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 java.util.ArrayList; +import java.util.List; + +@Getter +@Introspected +public class StudyQuery extends BrapiQuery { + private Boolean active; + private List commonCropNames; + private List externalReferenceIds; + private List externalReferenceSources; + private List germplasmDbIds; + private List germplasmNames; + private List locationDbIds; + private List locationNames; + private List observationVariableDbIds; + private List observationVariableNames; + private List observationVariablePUIs; + private List programDbIds; + private List programNames; + private List seasonDbIds; + private List studyCodes; + private List studyDbIds; + private List studyNames; + private List studyPUIs; + private List studyTypes; + private List trialDbIds; + private List trialNames; + + + // TODO: need a more dynamic way to build FilterField, FilterRequest only handles a subset of filtering. + public SearchRequest constructSearchRequest() { + List filters = new ArrayList<>(); +// if (getActive()) { +// filters.add(constructFilterRequest("active", getActive())); +// } +// if (!StringUtils.isBlank(getCommonCropNames())) { +// filters.add(constructFilterRequest("commonCropName", getCommonCropNames())); +// } +// if (!StringUtils.isBlank(getExternalReferenceIds())) { +// filters.add(constructFilterRequest("externalReferenceId", getExternalReferenceIds())); +// } +// if (!StringUtils.isBlank(getExternalReferenceSources())) { +// filters.add(constructFilterRequest("externalReferenceSource", getExternalReferenceSources())); +// } +// if (!StringUtils.isBlank(getGermplasmDbIds())) { +// filters.add(constructFilterRequest("germplasmDbId", getGermplasmDbIds())); +// } +// if (!StringUtils.isBlank(getGermplasmNames())) { +// filters.add(constructFilterRequest("germplasmName", getGermplasmNames())); +// } +// if (!StringUtils.isBlank(getLocationDbIds())) { +// filters.add(constructFilterRequest("locationDbId", getLocationDbIds())); +// } +// if (!StringUtils.isBlank(getLocationNames())) { +// filters.add(constructFilterRequest("locationName", getLocationNames())); +// } +// if (!StringUtils.isBlank(getObservationVariableDbIds())) { +// filters.add(constructFilterRequest("observationVariableDbId", getObservationVariableDbIds())); +// } +// if (!StringUtils.isBlank(getObservationVariableNames())) { +// filters.add(constructFilterRequest("observationVariableName", getObservationVariableNames())); +// } +// if (!StringUtils.isBlank(getObservationVariablePUIs())) { +// filters.add(constructFilterRequest("observationVariablePUI", getObservationVariablePUIs())); +// } +// if (!StringUtils.isBlank(getPage())) { +// filters.add(constructFilterRequest("page", getPage())); +// } +// if (!StringUtils.isBlank(getPageSize())) { +// filters.add(constructFilterRequest("pageSize", getPageSize())); +// } +// if (!StringUtils.isBlank(getProgramDbIds())) { +// filters.add(constructFilterRequest("programDbId", getProgramDbIds())); +// } +// if (!StringUtils.isBlank(getProgramNames())) { +// filters.add(constructFilterRequest("programName", getProgramNames())); +// } +// if (!StringUtils.isBlank(getSeasonDbIds())) { +// filters.add(constructFilterRequest("seasonDbId", getSeasonDbIds())); +// } +// if (!StringUtils.isBlank(getSortBy())) { +// filters.add(constructFilterRequest("sortBy", getSortBy())); +// } +// if (!StringUtils.isBlank(getSortOrder())) { +// filters.add(constructFilterRequest("sortOrder", getSortOrder())); +// } +// if (!StringUtils.isBlank(getStudyCodes())) { +// filters.add(constructFilterRequest("studyCode", getStudyCodes())); +// } +// if (!StringUtils.isBlank(getStudyDbIds())) { +// filters.add(constructFilterRequest("studyDbId", getStudyDbIds())); +// } +// if (!StringUtils.isBlank(getStudyNames())) { +// filters.add(constructFilterRequest("studyName", getStudyNames())); +// } +// if (!StringUtils.isBlank(getStudyPUIs())) { +// filters.add(constructFilterRequest("studyPUI", getStudyPUIs())); +// } +// if (!StringUtils.isBlank(getStudyTypes())) { +// filters.add(constructFilterRequest("studyType", getStudyTypes())); +// } +// if (!StringUtils.isBlank(getTrialDbIds())) { +// filters.add(constructFilterRequest("trialDbId", getTrialDbIds())); +// } +// if (!StringUtils.isBlank(getTrialNames())) { +// filters.add(constructFilterRequest("trialName", getTrialNames())); +// } + + return new SearchRequest(filters); + } +} diff --git a/src/main/java/org/breedinginsight/brapi/v2/services/BrAPIStudyService.java b/src/main/java/org/breedinginsight/brapi/v2/services/BrAPIStudyService.java new file mode 100644 index 000000000..08d94d7b8 --- /dev/null +++ b/src/main/java/org/breedinginsight/brapi/v2/services/BrAPIStudyService.java @@ -0,0 +1,59 @@ +/* + * 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.services; + +import io.micronaut.http.server.exceptions.InternalServerException; +import lombok.extern.slf4j.Slf4j; +import org.brapi.client.v2.model.exceptions.ApiException; +import org.brapi.v2.model.core.BrAPIStudy; +import org.breedinginsight.brapi.v2.dao.BrAPIStudyDAO; +import org.breedinginsight.services.exceptions.DoesNotExistException; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.List; +import java.util.UUID; + +@Slf4j +@Singleton +public class BrAPIStudyService { + + private final BrAPIStudyDAO studyDAO; + + @Inject + public BrAPIStudyService(BrAPIStudyDAO studyDAO) { + this.studyDAO = studyDAO; + } + + public List getStudies(UUID programId) throws ApiException { + try { + return studyDAO.getStudies(programId); + } catch (ApiException e) { + throw new InternalServerException(e.getMessage(), e); + } + } + + public BrAPIStudy getStudyByUUID(UUID programId, String studyId) throws DoesNotExistException { + try { + return studyDAO.getStudyByUUID(studyId, programId); + } catch (ApiException e) { + throw new InternalServerException(e.getMessage(), e); + } + } + +} diff --git a/src/main/java/org/breedinginsight/utilities/response/mappers/StudyQueryMapper.java b/src/main/java/org/breedinginsight/utilities/response/mappers/StudyQueryMapper.java new file mode 100644 index 000000000..41b1cd1c8 --- /dev/null +++ b/src/main/java/org/breedinginsight/utilities/response/mappers/StudyQueryMapper.java @@ -0,0 +1,54 @@ +/* + * 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.utilities.response.mappers; + +import lombok.Getter; +import org.brapi.v2.model.core.BrAPIStudy; +import org.breedinginsight.api.v1.controller.metadata.SortOrder; + +import javax.inject.Singleton; +import java.util.Map; +import java.util.function.Function; + + +@Getter +@Singleton +public class StudyQueryMapper extends AbstractQueryMapper { + + private final String defaultSortField = "studyName"; + private final SortOrder defaultSortOrder = SortOrder.ASC; + private Map> fields; + + public StudyQueryMapper() { + fields = Map.ofEntries( + Map.entry("studyName", BrAPIStudy::getStudyName), + Map.entry("trialDbId", BrAPIStudy::getTrialDbId) + ); + } + + @Override + public boolean exists(String fieldName) { + return getFields().containsKey(fieldName); + } + + @Override + public Function getField(String fieldName) throws NullPointerException { + if (fields.containsKey(fieldName)) return fields.get(fieldName); + else throw new NullPointerException(); + } +} From 7d0ea06d318b9d79ebd53bc7959f0a29a62e3706 Mon Sep 17 00:00:00 2001 From: mlm483 <128052931+mlm483@users.noreply.github.com> Date: Sun, 16 Jul 2023 16:14:20 -0400 Subject: [PATCH 2/6] [BI-1720] - TODO --- .../java/org/breedinginsight/brapi/v2/GermplasmController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/breedinginsight/brapi/v2/GermplasmController.java b/src/main/java/org/breedinginsight/brapi/v2/GermplasmController.java index d91f4ed57..e27715e2e 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/GermplasmController.java +++ b/src/main/java/org/breedinginsight/brapi/v2/GermplasmController.java @@ -72,6 +72,7 @@ public GermplasmController(BrAPIGermplasmService germplasmService, GermplasmQuer this.brAPIEndpointProvider = brAPIEndpointProvider; } + // TODO: remove or update. I don't think this is used, and it doesn't properly support BrAPI request body. @Post("/programs/{programId}" + BrapiVersion.BRAPI_V2 + "/search/germplasm{?queryParams*}") @Produces(MediaType.APPLICATION_JSON) @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL}) From 11172c82762072847cd9e11cdca7403588b3d65c Mon Sep 17 00:00:00 2001 From: mlm483 <128052931+mlm483@users.noreply.github.com> Date: Sun, 16 Jul 2023 16:15:01 -0400 Subject: [PATCH 3/6] [BI-1720] - DRY --- .../breedinginsight/brapps/importer/daos/BrAPITrialDAO.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/org/breedinginsight/brapps/importer/daos/BrAPITrialDAO.java b/src/main/java/org/breedinginsight/brapps/importer/daos/BrAPITrialDAO.java index 0a9180936..ea37e22b7 100644 --- a/src/main/java/org/breedinginsight/brapps/importer/daos/BrAPITrialDAO.java +++ b/src/main/java/org/breedinginsight/brapps/importer/daos/BrAPITrialDAO.java @@ -38,15 +38,11 @@ @Singleton public class BrAPITrialDAO { - @Property(name = "brapi.server.reference-source") - private String BRAPI_REFERENCE_SOURCE; - private final ProgramDAO programDAO; private final ImportDAO importDAO; private final BrAPIDAOUtil brAPIDAOUtil; private final ProgramService programService; private final BrAPIEndpointProvider brAPIEndpointProvider; - private final String referenceSource; @Inject @@ -138,7 +134,7 @@ private List processExperimentsForDisplay(List trials, S public Optional getTrialById(UUID programId, UUID trialDbId) throws ApiException, DoesNotExistException { Program program = programService.getById(programId).orElseThrow(() -> new DoesNotExistException("Program id does not exist")); - String refSoure = Utilities.generateReferenceSource(BRAPI_REFERENCE_SOURCE, ExternalReferenceSource.TRIALS); + String refSoure = Utilities.generateReferenceSource(referenceSource, ExternalReferenceSource.TRIALS); List trials = getTrialsByExRef(refSoure, trialDbId.toString(), program); return Utilities.getSingleOptional(trials); From 84f7f5481971bf96887f6fde1897b446da9c679a Mon Sep 17 00:00:00 2001 From: mlm483 <128052931+mlm483@users.noreply.github.com> Date: Tue, 18 Jul 2023 17:10:54 -0400 Subject: [PATCH 4/6] [BI-1720] - study GET endpoint filtering working --- .../brapi/v2/StudyController.java | 27 +--- .../v2/model/request/query/StudyQuery.java | 132 +++++------------- .../brapi/v2/services/BrAPITrialService.java | 26 ++-- .../breedinginsight/utilities/Utilities.java | 36 ++++- .../response/mappers/StudyQueryMapper.java | 10 +- 5 files changed, 96 insertions(+), 135 deletions(-) diff --git a/src/main/java/org/breedinginsight/brapi/v2/StudyController.java b/src/main/java/org/breedinginsight/brapi/v2/StudyController.java index 69e5697a5..26758084c 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/StudyController.java +++ b/src/main/java/org/breedinginsight/brapi/v2/StudyController.java @@ -17,7 +17,6 @@ import org.breedinginsight.api.model.v1.response.Response; import org.breedinginsight.api.model.v1.validators.QueryValid; import org.breedinginsight.brapi.v1.controller.BrapiVersion; -import org.breedinginsight.brapi.v1.model.request.query.BrapiQuery; import org.breedinginsight.brapi.v2.model.request.query.StudyQuery; import org.breedinginsight.brapi.v2.services.BrAPIStudyService; import org.breedinginsight.services.exceptions.DoesNotExistException; @@ -44,26 +43,6 @@ public StudyController(BrAPIStudyService studyService, StudyQueryMapper studyQue this.studyQueryMapper = studyQueryMapper; } - @Post("/programs/{programId}" + BrapiVersion.BRAPI_V2 + "/search/studies{?queryParams*}") - @Produces(MediaType.APPLICATION_JSON) - @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL}) - public HttpResponse>>> searchStudy( - @PathVariable("programId") UUID programId, - @QueryValue @QueryValid(using = StudyQueryMapper.class) @Valid BrapiQuery queryParams, - @Body @Valid StudyQuery body) { - try { - log.debug("fetching studies for program: " + programId); - List studies = studyService.getStudies(programId); - queryParams.setSortField(studyQueryMapper.getDefaultSortField()); - queryParams.setSortOrder(studyQueryMapper.getDefaultSortOrder()); - // Filter the response. - return ResponseUtils.getBrapiQueryResponse(studies, studyQueryMapper, queryParams, body.constructSearchRequest()); - } catch (ApiException e) { - log.info(e.getMessage(), e); - return HttpResponse.status(HttpStatus.INTERNAL_SERVER_ERROR, "Error retrieving study"); - } - } - @Get("/programs/{programId}" + BrapiVersion.BRAPI_V2 + "/studies{?queryParams*}") @Produces(MediaType.APPLICATION_JSON) @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL}) @@ -73,9 +52,11 @@ public HttpResponse>>> getStudy( try { log.debug("fetching studies for program: " + programId); - List study = studyService.getStudies(programId); + List studies = studyService.getStudies(programId); + queryParams.setSortField(studyQueryMapper.getDefaultSortField()); + queryParams.setSortOrder(studyQueryMapper.getDefaultSortOrder()); SearchRequest searchRequest = queryParams.constructSearchRequest(); - return ResponseUtils.getBrapiQueryResponse(study, studyQueryMapper, queryParams, searchRequest); + return ResponseUtils.getBrapiQueryResponse(studies, studyQueryMapper, queryParams, searchRequest); } catch (ApiException e) { log.info(e.getMessage(), e); return HttpResponse.status(HttpStatus.INTERNAL_SERVER_ERROR, "Error retrieving study"); diff --git a/src/main/java/org/breedinginsight/brapi/v2/model/request/query/StudyQuery.java b/src/main/java/org/breedinginsight/brapi/v2/model/request/query/StudyQuery.java index 57c079d91..c3115f05e 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/model/request/query/StudyQuery.java +++ b/src/main/java/org/breedinginsight/brapi/v2/model/request/query/StudyQuery.java @@ -5,6 +5,7 @@ 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; @@ -12,108 +13,41 @@ @Getter @Introspected public class StudyQuery extends BrapiQuery { - private Boolean active; - private List commonCropNames; - private List externalReferenceIds; - private List externalReferenceSources; - private List germplasmDbIds; - private List germplasmNames; - private List locationDbIds; - private List locationNames; - private List observationVariableDbIds; - private List observationVariableNames; - private List observationVariablePUIs; - private List programDbIds; - private List programNames; - private List seasonDbIds; - private List studyCodes; - private List studyDbIds; - private List studyNames; - private List studyPUIs; - private List studyTypes; - private List trialDbIds; - private List trialNames; + private String studyType; + private String locationDbId; + private String studyCode; + private String studyPUI; + private String commonCropName; + private String trialDbId; + private String studyDbId; + private String studyName; - - // TODO: need a more dynamic way to build FilterField, FilterRequest only handles a subset of filtering. public SearchRequest constructSearchRequest() { List filters = new ArrayList<>(); -// if (getActive()) { -// filters.add(constructFilterRequest("active", getActive())); -// } -// if (!StringUtils.isBlank(getCommonCropNames())) { -// filters.add(constructFilterRequest("commonCropName", getCommonCropNames())); -// } -// if (!StringUtils.isBlank(getExternalReferenceIds())) { -// filters.add(constructFilterRequest("externalReferenceId", getExternalReferenceIds())); -// } -// if (!StringUtils.isBlank(getExternalReferenceSources())) { -// filters.add(constructFilterRequest("externalReferenceSource", getExternalReferenceSources())); -// } -// if (!StringUtils.isBlank(getGermplasmDbIds())) { -// filters.add(constructFilterRequest("germplasmDbId", getGermplasmDbIds())); -// } -// if (!StringUtils.isBlank(getGermplasmNames())) { -// filters.add(constructFilterRequest("germplasmName", getGermplasmNames())); -// } -// if (!StringUtils.isBlank(getLocationDbIds())) { -// filters.add(constructFilterRequest("locationDbId", getLocationDbIds())); -// } -// if (!StringUtils.isBlank(getLocationNames())) { -// filters.add(constructFilterRequest("locationName", getLocationNames())); -// } -// if (!StringUtils.isBlank(getObservationVariableDbIds())) { -// filters.add(constructFilterRequest("observationVariableDbId", getObservationVariableDbIds())); -// } -// if (!StringUtils.isBlank(getObservationVariableNames())) { -// filters.add(constructFilterRequest("observationVariableName", getObservationVariableNames())); -// } -// if (!StringUtils.isBlank(getObservationVariablePUIs())) { -// filters.add(constructFilterRequest("observationVariablePUI", getObservationVariablePUIs())); -// } -// if (!StringUtils.isBlank(getPage())) { -// filters.add(constructFilterRequest("page", getPage())); -// } -// if (!StringUtils.isBlank(getPageSize())) { -// filters.add(constructFilterRequest("pageSize", getPageSize())); -// } -// if (!StringUtils.isBlank(getProgramDbIds())) { -// filters.add(constructFilterRequest("programDbId", getProgramDbIds())); -// } -// if (!StringUtils.isBlank(getProgramNames())) { -// filters.add(constructFilterRequest("programName", getProgramNames())); -// } -// if (!StringUtils.isBlank(getSeasonDbIds())) { -// filters.add(constructFilterRequest("seasonDbId", getSeasonDbIds())); -// } -// if (!StringUtils.isBlank(getSortBy())) { -// filters.add(constructFilterRequest("sortBy", getSortBy())); -// } -// if (!StringUtils.isBlank(getSortOrder())) { -// filters.add(constructFilterRequest("sortOrder", getSortOrder())); -// } -// if (!StringUtils.isBlank(getStudyCodes())) { -// filters.add(constructFilterRequest("studyCode", getStudyCodes())); -// } -// if (!StringUtils.isBlank(getStudyDbIds())) { -// filters.add(constructFilterRequest("studyDbId", getStudyDbIds())); -// } -// if (!StringUtils.isBlank(getStudyNames())) { -// filters.add(constructFilterRequest("studyName", getStudyNames())); -// } -// if (!StringUtils.isBlank(getStudyPUIs())) { -// filters.add(constructFilterRequest("studyPUI", getStudyPUIs())); -// } -// if (!StringUtils.isBlank(getStudyTypes())) { -// filters.add(constructFilterRequest("studyType", getStudyTypes())); -// } -// if (!StringUtils.isBlank(getTrialDbIds())) { -// filters.add(constructFilterRequest("trialDbId", getTrialDbIds())); -// } -// if (!StringUtils.isBlank(getTrialNames())) { -// filters.add(constructFilterRequest("trialName", getTrialNames())); -// } - + if (!StringUtils.isBlank(getStudyType())) { + filters.add(constructFilterRequest("studyType", getStudyType())); + } + if (!StringUtils.isBlank(getLocationDbId())) { + filters.add(constructFilterRequest("locationDbId", getLocationDbId())); + } + if (!StringUtils.isBlank(getStudyCode())) { + filters.add(constructFilterRequest("studyCode", getStudyCode())); + } + if (!StringUtils.isBlank(getStudyPUI())) { + filters.add(constructFilterRequest("studyPUI", getStudyPUI())); + } + if (!StringUtils.isBlank(getCommonCropName())) { + filters.add(constructFilterRequest("commonCropName", getCommonCropName())); + } + if (!StringUtils.isBlank(getTrialDbId())) { + filters.add(constructFilterRequest("trialDbId", getTrialDbId())); + } + if (!StringUtils.isBlank(getStudyDbId())) { + filters.add(constructFilterRequest("studyDbId", getStudyDbId())); + } + if (!StringUtils.isBlank(getStudyName())) { + filters.add(constructFilterRequest("studyName", getStudyName())); + } return new SearchRequest(filters); } } diff --git a/src/main/java/org/breedinginsight/brapi/v2/services/BrAPITrialService.java b/src/main/java/org/breedinginsight/brapi/v2/services/BrAPITrialService.java index 0126926a4..c4c5576dd 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/services/BrAPITrialService.java +++ b/src/main/java/org/breedinginsight/brapi/v2/services/BrAPITrialService.java @@ -1,4 +1,5 @@ package org.breedinginsight.brapi.v2.services; + import io.micronaut.context.annotation.Property; import io.micronaut.http.MediaType; import io.micronaut.http.server.exceptions.InternalServerException; @@ -28,6 +29,7 @@ import org.breedinginsight.services.writers.CSVWriter; import org.breedinginsight.services.writers.ExcelWriter; import org.breedinginsight.utilities.Utilities; + import javax.inject.Inject; import javax.inject.Singleton; import java.io.IOException; @@ -226,7 +228,7 @@ public DownloadFile exportObservations( else { // Zip, as there are multiple files. StreamedFile zipFile = zipFiles(files); - downloadFile = new DownloadFile(makeZipFileName(experiment), zipFile); + downloadFile = new DownloadFile(makeZipFileName(experiment, program), zipFile); } } else { List> exportRows = new ArrayList<>(rowByOUId.values()); @@ -465,22 +467,26 @@ private void addObsVarColumns( } } private String makeFileName(BrAPITrial experiment, Program program, String envName) { - // _Observation Dataset [-]__ - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd:hh-mm-ssZ"); + // _Observation Dataset__ + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_hh-mm-ssZ"); String timestamp = formatter.format(OffsetDateTime.now()); - return String.format("%s_Observation Dataset [%s-%s]_%s_%s", + String unsafeName = String.format("%s_Observation Dataset_%s_%s", Utilities.removeProgramKey(experiment.getTrialName(), program.getKey()), - program.getKey(), - experiment.getAdditionalInfo().getAsJsonObject().get(BrAPIAdditionalInfoFields.EXPERIMENT_NUMBER).getAsString(), - envName, + Utilities.removeProgramKeyAndUnknownAdditionalData(envName, program.getKey()), timestamp); + // Make file name safe for all platforms. + return Utilities.makePortableFilename(unsafeName); } - private String makeZipFileName(BrAPITrial experiment) { + private String makeZipFileName(BrAPITrial experiment, Program program) { // .zip - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd:hh-mm-ssZ"); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_hh-mm-ssZ"); String timestamp = formatter.format(OffsetDateTime.now()); - return String.format("%s_%s.zip", experiment.getTrialName(), timestamp); + String unsafeName = String.format("%s_%s.zip", + Utilities.removeProgramKey(experiment.getTrialName(), program.getKey()), + timestamp); + // Make file name safe for all platforms. + return Utilities.makePortableFilename(unsafeName); } private List filterDatasetByEnvironment( diff --git a/src/main/java/org/breedinginsight/utilities/Utilities.java b/src/main/java/org/breedinginsight/utilities/Utilities.java index 52e85ad2d..9ce3a1b9e 100644 --- a/src/main/java/org/breedinginsight/utilities/Utilities.java +++ b/src/main/java/org/breedinginsight/utilities/Utilities.java @@ -20,7 +20,6 @@ import org.apache.commons.lang3.StringUtils; import org.brapi.client.v2.model.exceptions.ApiException; import org.brapi.v2.model.BrAPIExternalReference; -import org.brapi.v2.model.germ.BrAPIGermplasmSynonyms; import org.breedinginsight.brapps.importer.services.ExternalReferenceSource; import java.util.List; @@ -152,4 +151,39 @@ public static Optional getSingleOptional(List items) { return Optional.empty(); } } + + /** + * For a possibly unsafe file name, return a new String that is safe across platforms. + * @param name a possibly unsafe file name + * @return a portable file name + */ + public static String makePortableFilename(String name) { + StringBuilder sb = new StringBuilder(); + char c; + char last_appended = '_'; + int i = 0; + while (i < name.length()) { + c = name.charAt(i); + if (isSafeChar(c)) { + sb.append(c); + last_appended = c; + } + else { + // Replace illegal chars with '_', but prevent repeat underscores. + if (last_appended != '_') { + sb.append('_'); + last_appended = '_'; + } + } + ++i; + } + + return sb.toString(); + } + + private static boolean isSafeChar(char c) { + // Check if c is in the portable filename character set. + // See https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282 + return Character.isLetterOrDigit(c) || c == '-' || c == '_' || c == '.'; + } } diff --git a/src/main/java/org/breedinginsight/utilities/response/mappers/StudyQueryMapper.java b/src/main/java/org/breedinginsight/utilities/response/mappers/StudyQueryMapper.java index 41b1cd1c8..96311c00b 100644 --- a/src/main/java/org/breedinginsight/utilities/response/mappers/StudyQueryMapper.java +++ b/src/main/java/org/breedinginsight/utilities/response/mappers/StudyQueryMapper.java @@ -36,8 +36,14 @@ public class StudyQueryMapper extends AbstractQueryMapper { public StudyQueryMapper() { fields = Map.ofEntries( - Map.entry("studyName", BrAPIStudy::getStudyName), - Map.entry("trialDbId", BrAPIStudy::getTrialDbId) + Map.entry("studyType", BrAPIStudy::getStudyType), + Map.entry("locationDbId", BrAPIStudy::getLocationDbId), + Map.entry("studyCode", BrAPIStudy::getStudyCode), + Map.entry("studyPUI", BrAPIStudy::getStudyPUI), + Map.entry("commonCropName", BrAPIStudy::getCommonCropName), + Map.entry("trialDbId", BrAPIStudy::getTrialDbId), + Map.entry("studyDbId", BrAPIStudy::getStudyDbId), + Map.entry("studyName", BrAPIStudy::getStudyName) ); } From 6d3040e6249f6bd005fd656388dfcccccce686fa Mon Sep 17 00:00:00 2001 From: mlm483 <128052931+mlm483@users.noreply.github.com> Date: Tue, 18 Jul 2023 17:39:29 -0400 Subject: [PATCH 5/6] [BI-1720] - removed unused controller method --- .../brapi/v2/StudyController.java | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/src/main/java/org/breedinginsight/brapi/v2/StudyController.java b/src/main/java/org/breedinginsight/brapi/v2/StudyController.java index 26758084c..27a9033aa 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/StudyController.java +++ b/src/main/java/org/breedinginsight/brapi/v2/StudyController.java @@ -65,24 +65,4 @@ public HttpResponse>>> getStudy( return HttpResponse.status(HttpStatus.UNPROCESSABLE_ENTITY, "Error parsing requested date format"); } } - - @Get("/programs/{programId}" + BrapiVersion.BRAPI_V2 + "/studies/{studyId}") - @Produces(MediaType.APPLICATION_JSON) - @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL}) - public HttpResponse> getSingleStudy( - @PathVariable("programId") UUID programId, - @PathVariable("studyId") String studyId) { - try { - log.debug("fetching study id:" + studyId +" for program: " + programId); - Response response = new Response(studyService.getStudyByUUID(programId, studyId)); - return HttpResponse.ok(response); - } catch (InternalServerException e) { - log.info(e.getMessage(), e); - return HttpResponse.status(HttpStatus.INTERNAL_SERVER_ERROR, "Error retrieving study"); - } catch (DoesNotExistException e) { - log.info(e.getMessage(), e); - return HttpResponse.status(HttpStatus.NOT_FOUND, "Study not found"); - } - } - } From 6e0b36f81f6f9fa3199fc8de6cf63c07b6fa4535 Mon Sep 17 00:00:00 2001 From: mlm483 <128052931+mlm483@users.noreply.github.com> Date: Tue, 18 Jul 2023 17:42:45 -0400 Subject: [PATCH 6/6] [BI-1720] - updated TODO --- .../java/org/breedinginsight/brapi/v2/GermplasmController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/breedinginsight/brapi/v2/GermplasmController.java b/src/main/java/org/breedinginsight/brapi/v2/GermplasmController.java index e27715e2e..5a022d88d 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/GermplasmController.java +++ b/src/main/java/org/breedinginsight/brapi/v2/GermplasmController.java @@ -72,7 +72,7 @@ public GermplasmController(BrAPIGermplasmService germplasmService, GermplasmQuer this.brAPIEndpointProvider = brAPIEndpointProvider; } - // TODO: remove or update. I don't think this is used, and it doesn't properly support BrAPI request body. + // TODO: expand to fully support BrAPI request body. @Post("/programs/{programId}" + BrapiVersion.BRAPI_V2 + "/search/germplasm{?queryParams*}") @Produces(MediaType.APPLICATION_JSON) @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL})