diff --git a/src/main/java/org/breedinginsight/api/v1/controller/OntologyController.java b/src/main/java/org/breedinginsight/api/v1/controller/OntologyController.java index cd02ef6ea..d6b0e6a5d 100644 --- a/src/main/java/org/breedinginsight/api/v1/controller/OntologyController.java +++ b/src/main/java/org/breedinginsight/api/v1/controller/OntologyController.java @@ -4,12 +4,9 @@ import io.micronaut.http.HttpStatus; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.*; -import io.micronaut.security.annotation.Secured; -import io.micronaut.security.rules.SecurityRule; import lombok.extern.slf4j.Slf4j; import org.breedinginsight.api.auth.ProgramSecured; import org.breedinginsight.api.auth.ProgramSecuredRole; -import org.breedinginsight.api.auth.ProgramSecuredRoleGroup; import org.breedinginsight.api.auth.SecurityService; import org.breedinginsight.api.model.v1.request.SharedOntologyProgramRequest; import org.breedinginsight.api.model.v1.response.DataResponse; @@ -19,8 +16,8 @@ import org.breedinginsight.api.model.v1.response.metadata.Status; import org.breedinginsight.api.model.v1.response.metadata.StatusCode; import org.breedinginsight.api.v1.controller.metadata.AddMetadata; -import org.breedinginsight.model.SharedProgram; -import org.breedinginsight.model.Trait; +import org.breedinginsight.model.SharedOntology; +import org.breedinginsight.model.SubscribedOntology; import org.breedinginsight.services.OntologyService; import org.breedinginsight.services.exceptions.DoesNotExistException; import org.breedinginsight.services.exceptions.UnprocessableEntityException; @@ -67,15 +64,20 @@ public OntologyController(SecurityService securityService, OntologyService ontol @Produces(MediaType.APPLICATION_JSON) @AddMetadata @ProgramSecured(roles = {ProgramSecuredRole.BREEDER}) - public HttpResponse>> getAvailablePrograms( + public HttpResponse>> getAvailablePrograms( @PathVariable UUID programId, @QueryValue(defaultValue = "false") Boolean shared) { - List sharedPrograms = ontologyService.getSharedOntology(programId, shared); - List metadataStatus = new ArrayList<>(); - metadataStatus.add(new Status(StatusCode.INFO, "Successful Query")); - Pagination pagination = new Pagination(sharedPrograms.size(), 1, 1, 0); - Metadata metadata = new Metadata(pagination, metadataStatus); - Response> response = new Response(metadata, new DataResponse<>(sharedPrograms)); - return HttpResponse.ok(response); + try { + List sharedOntologies = ontologyService.getSharedOntology(programId, shared); + List metadataStatus = new ArrayList<>(); + metadataStatus.add(new Status(StatusCode.INFO, "Successful Query")); + Pagination pagination = new Pagination(sharedOntologies.size(), 1, 1, 0); + Metadata metadata = new Metadata(pagination, metadataStatus); + Response> response = new Response(metadata, new DataResponse<>(sharedOntologies)); + return HttpResponse.ok(response); + } catch (DoesNotExistException e) { + log.error(e.getMessage(), e); + return HttpResponse.status(HttpStatus.NOT_FOUND, e.getMessage()); + } } /** @@ -94,22 +96,26 @@ public HttpResponse>> getAvailablePrograms( @Post("/programs/{programId}/ontology/shared/programs") @Produces(MediaType.APPLICATION_JSON) @ProgramSecured(roles = {ProgramSecuredRole.BREEDER}) - public HttpResponse>> shareOntology( + public HttpResponse>> shareOntology( @PathVariable UUID programId, @Body List request) { try { - List sharedPrograms = ontologyService.shareOntology(programId, securityService.getUser(), request); - List metadataStatus = new ArrayList<>(); + List sharedOntologies = ontologyService.shareOntology(programId, securityService.getUser(), request); + List metadataStatus = new ArrayList<>(); metadataStatus.add(new Status(StatusCode.INFO, "Successful Creation")); - Pagination pagination = new Pagination(sharedPrograms.size(), 1, 1, 0); + Pagination pagination = new Pagination(sharedOntologies.size(), 1, 1, 0); Metadata metadata = new Metadata(pagination, metadataStatus); - Response> response = new Response(metadata, new DataResponse<>(sharedPrograms)); + Response> response = new Response(metadata, new DataResponse<>(sharedOntologies)); return HttpResponse.ok(response); } catch (ValidatorException e) { log.error("Validation errors", e); HttpResponse response = HttpResponse.status(HttpStatus.UNPROCESSABLE_ENTITY).body(e.getErrors()); return response; } catch (UnprocessableEntityException e) { + log.error(e.getMessage(), e); return HttpResponse.status(HttpStatus.UNPROCESSABLE_ENTITY, e.getMessage()); + } catch (DoesNotExistException e) { + log.error(e.getMessage(), e); + return HttpResponse.status(HttpStatus.NOT_FOUND, e.getMessage()); } } @@ -124,14 +130,99 @@ public HttpResponse>> shareOntology( @Delete("/programs/{programId}/ontology/shared/programs/{sharedProgramId}") @Produces(MediaType.APPLICATION_JSON) @ProgramSecured(roles = {ProgramSecuredRole.BREEDER}) - public HttpResponse>> revokeOntology( + public HttpResponse revokeOntology( @PathVariable UUID programId, @PathVariable UUID sharedProgramId) { try { ontologyService.revokeOntology(programId, sharedProgramId); return HttpResponse.ok(); } catch (UnprocessableEntityException e) { + log.error(e.getMessage(), e); + return HttpResponse.status(HttpStatus.UNPROCESSABLE_ENTITY, e.getMessage()); + } catch (DoesNotExistException e) { + log.error(e.getMessage(), e); + return HttpResponse.status(HttpStatus.NOT_FOUND, e.getMessage()); + } + } + + /** + * Scoped under the program that is subscribing. Accept a shared ontology. + * + * @param programId -- Program that is subscribing to an ontology + * @param sharingProgramId -- Program that has shared its ontology. + * @return + */ + @Put("/programs/{programId}/ontology/subscribe/{sharingProgramId}") + @Produces(MediaType.APPLICATION_JSON) + @AddMetadata + @ProgramSecured(roles = {ProgramSecuredRole.BREEDER}) + public HttpResponse> subscribeOntology( + @PathVariable UUID programId, @PathVariable UUID sharingProgramId) { + try { + SubscribedOntology shareRequest = ontologyService.subscribeOntology(programId, sharingProgramId); + Response response = new Response(shareRequest); + return HttpResponse.ok(response); + } catch (UnprocessableEntityException e) { + log.error(e.getMessage(), e); + return HttpResponse.status(HttpStatus.UNPROCESSABLE_ENTITY, e.getMessage()); + } catch (DoesNotExistException e) { + log.error(e.getMessage(), e); + return HttpResponse.status(HttpStatus.NOT_FOUND, e.getMessage()); + } + } + + /** + * Scoped under the program that is subscribing. Unsubscribe from a shared ontology. + * + * @param programId -- Program that is unsubscribing from an ontology + * @param sharingProgramId -- Program that has shared its ontology. + * @return + */ + @Delete("/programs/{programId}/ontology/subscribe/{sharingProgramId}") + @Produces(MediaType.APPLICATION_JSON) + @ProgramSecured(roles = {ProgramSecuredRole.BREEDER}) + public HttpResponse unsubscribeOntology( + @PathVariable UUID programId, @PathVariable UUID sharingProgramId) { + try { + ontologyService.unsubscribeOntology(programId, sharingProgramId); + return HttpResponse.ok(); + } catch (UnprocessableEntityException e) { + log.error(e.getMessage(), e); return HttpResponse.status(HttpStatus.UNPROCESSABLE_ENTITY, e.getMessage()); } catch (DoesNotExistException e) { + log.error(e.getMessage(), e); + return HttpResponse.status(HttpStatus.NOT_FOUND, e.getMessage()); + } + } + + /** + * Retrieves subscription options for programs that have shared their ontology with the requesting program. + * Will indicate whether the program has subscribed to a given ontology or not. + * + * @param programId -- Program request information + * @return + * { + * programId, -- Program that owns the ontology the request program is subscribed to. + * programName, + * subscribed, -- boolean. Whether the requesting program is subscribed to this ontology or not. + * editable -- boolean || null. Indicates whether this program can unsubscribe from this ontology or not. Null if not subscribed to this ontology. + * } + */ + @Get("/programs/{programId}/ontology/subscribe") + @Produces(MediaType.APPLICATION_JSON) + @AddMetadata + @ProgramSecured(roles = {ProgramSecuredRole.BREEDER}) + public HttpResponse>> getSubscribedOntology( + @PathVariable UUID programId) { + try { + List shareRequests = ontologyService.getSubscribeOntologyOptions(programId); + List metadataStatus = new ArrayList<>(); + metadataStatus.add(new Status(StatusCode.INFO, "Successful Creation")); + Pagination pagination = new Pagination(shareRequests.size(), 1, 1, 0); + Metadata metadata = new Metadata(pagination, metadataStatus); + Response> response = new Response(metadata, new DataResponse<>(shareRequests)); + return HttpResponse.ok(response); + } catch (DoesNotExistException e) { + log.error(e.getMessage(), e); return HttpResponse.status(HttpStatus.NOT_FOUND, e.getMessage()); } } diff --git a/src/main/java/org/breedinginsight/daos/ObservationDAO.java b/src/main/java/org/breedinginsight/daos/ObservationDAO.java index b6f7190ae..2dbaab866 100644 --- a/src/main/java/org/breedinginsight/daos/ObservationDAO.java +++ b/src/main/java/org/breedinginsight/daos/ObservationDAO.java @@ -33,6 +33,7 @@ import javax.inject.Inject; import java.util.List; import java.util.Optional; +import java.util.UUID; import static org.brapi.v2.model.BrAPIWSMIMEDataTypes.APPLICATION_JSON; @@ -77,6 +78,25 @@ public List getObservationsByVariableDbIds(List observ } + public List getObservationsByVariableAndBrAPIProgram(String brapiProgramId, List observationVariableDbIds) { + + try { + BrAPIObservationSearchRequest request = new BrAPIObservationSearchRequest() + .observationVariableDbIds(observationVariableDbIds) + .programDbIds(List.of(brapiProgramId)); + + ObservationsApi api = brAPIProvider.getObservationsAPI(BrAPIClientType.PHENO); + return BrAPIDAOUtil.search( + api::searchObservationsPost, + this::searchObservationsSearchResultsDbIdGet, + request + ); + } catch (ApiException e) { + throw new InternalServerException("Observations brapi search error", e); + } + + } + private ApiResponse, Optional>> searchObservationsSearchResultsDbIdGet(String searchResultsDbId, Integer page, Integer pageSize) throws ApiException { ObservationsApi api = brAPIProvider.getObservationsAPI(BrAPIClientType.PHENO); diff --git a/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java b/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java index 2068f34cf..3d11e6518 100644 --- a/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java +++ b/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java @@ -62,11 +62,25 @@ public void revokeSharedOntology(ProgramSharedOntologyEntity sharedOntology) { programSharedOntologyDao.delete(sharedOntology); } - public boolean programSubscribedSharedOntology(UUID programId) { - List shareRecords = programSharedOntologyDao.fetchBySharedProgramId(programId); - return !shareRecords.stream() + public Optional getSubscribedSharedOntology(UUID programId) { + List shareRecords = programSharedOntologyDao.fetchBySharedProgramId(programId).stream() .filter(shareRecord -> shareRecord.getActive()) - .collect(Collectors.toList()) - .isEmpty(); + .collect(Collectors.toList()); + + return shareRecords.size() > 0 ? Optional.of(shareRecords.get(0)) : Optional.empty(); + } + + public void acceptSharedOntology(ProgramSharedOntologyEntity sharedOntology) { + sharedOntology.setActive(true); + programSharedOntologyDao.update(sharedOntology); + } + + public void denySharedOntology(ProgramSharedOntologyEntity sharedOntology) { + sharedOntology.setActive(false); + programSharedOntologyDao.update(sharedOntology); + } + + public List getSubscriptionOptions(UUID programId) { + return programSharedOntologyDao.fetchBySharedProgramId(programId); } } diff --git a/src/main/java/org/breedinginsight/daos/TraitDAO.java b/src/main/java/org/breedinginsight/daos/TraitDAO.java index fd73a4f88..fdf75e50f 100644 --- a/src/main/java/org/breedinginsight/daos/TraitDAO.java +++ b/src/main/java/org/breedinginsight/daos/TraitDAO.java @@ -218,7 +218,28 @@ public List getObservationsForTraits(List traitIds) { return observationDao.getObservationsByVariableDbIds(brapiVariableIds); } + public List getObservationsForTraitsByBrAPIProgram(String brapiProgramId, List traitIds) { + + List ids = traitIds.stream() + .map(trait -> trait.toString()) + .collect(Collectors.toList()); + + List variables = searchVariables(ids); + + // TODO: make sure have all expected external references + if (variables.size() != ids.size()) { + throw new InternalServerException("Observation variables search results mismatch"); + } + + List brapiVariableIds = variables.stream() + .map(variable -> variable.getObservationVariableDbId()).collect(Collectors.toList()); + + return observationDao.getObservationsByVariableAndBrAPIProgram(brapiProgramId, brapiVariableIds); + } + public List searchVariables(List variableIds) { + + if (variableIds == null || variableIds.size() == 0) return new ArrayList<>(); try { BrAPIObservationVariableSearchRequest request = new BrAPIObservationVariableSearchRequest() .externalReferenceIDs(variableIds); @@ -573,5 +594,4 @@ private void saturateTrait(Trait trait, BrAPIObservationVariable brApiVariable) Scale scale = trait.getScale(); scale.setBrAPIProperties(brApiVariable.getScale()); } - } diff --git a/src/main/java/org/breedinginsight/model/SharedProgram.java b/src/main/java/org/breedinginsight/model/SharedOntology.java similarity index 90% rename from src/main/java/org/breedinginsight/model/SharedProgram.java rename to src/main/java/org/breedinginsight/model/SharedOntology.java index 24c12f0ea..54cfc12d6 100644 --- a/src/main/java/org/breedinginsight/model/SharedProgram.java +++ b/src/main/java/org/breedinginsight/model/SharedOntology.java @@ -9,7 +9,7 @@ @Builder @AllArgsConstructor @NoArgsConstructor -public class SharedProgram { +public class SharedOntology { private UUID programId; private String programName; private Boolean shared; diff --git a/src/main/java/org/breedinginsight/model/SubscribedOntology.java b/src/main/java/org/breedinginsight/model/SubscribedOntology.java new file mode 100644 index 000000000..54c664e85 --- /dev/null +++ b/src/main/java/org/breedinginsight/model/SubscribedOntology.java @@ -0,0 +1,17 @@ +package org.breedinginsight.model; + +import lombok.*; + +import java.util.UUID; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SubscribedOntology { + private UUID programId; + private String programName; + private Boolean subscribed; + private Boolean editable; +} diff --git a/src/main/java/org/breedinginsight/services/OntologyService.java b/src/main/java/org/breedinginsight/services/OntologyService.java index 8a515e0cb..b4687a1cd 100644 --- a/src/main/java/org/breedinginsight/services/OntologyService.java +++ b/src/main/java/org/breedinginsight/services/OntologyService.java @@ -1,7 +1,8 @@ package org.breedinginsight.services; import io.micronaut.http.HttpStatus; -import io.micronaut.http.exceptions.HttpStatusException; +import io.micronaut.http.server.exceptions.InternalServerException; +import org.brapi.v2.model.core.BrAPIProgram; import org.breedinginsight.api.auth.AuthenticatedUser; import org.breedinginsight.api.model.v1.request.SharedOntologyProgramRequest; import org.breedinginsight.api.model.v1.response.ValidationError; @@ -11,7 +12,9 @@ import org.breedinginsight.daos.ProgramOntologyDAO; import org.breedinginsight.daos.TraitDAO; import org.breedinginsight.model.Program; -import org.breedinginsight.model.SharedProgram; +import org.breedinginsight.model.SharedOntology; +import org.breedinginsight.model.SubscribedOntology; +import org.breedinginsight.model.Trait; import org.breedinginsight.services.exceptions.DoesNotExistException; import org.breedinginsight.services.exceptions.UnprocessableEntityException; import org.breedinginsight.services.exceptions.ValidatorException; @@ -44,13 +47,13 @@ public OntologyService(ProgramDAO programDAO, ProgramOntologyDAO programOntology * @param sharedOnly -- True = return only shared programs, False = get all shareable programs * @return List */ - public List getSharedOntology(@NotNull UUID programId, @NotNull Boolean sharedOnly) { + public List getSharedOntology(@NotNull UUID programId, @NotNull Boolean sharedOnly) throws DoesNotExistException { // Get program with that id Program program = getProgram(programId); - List formattedPrograms = getSharedProgramsFormatted(program); - Set sharedProgramIds = formattedPrograms.stream().map(SharedProgram::getProgramId).collect(Collectors.toSet()); + List formattedPrograms = getSharedProgramsFormatted(program); + Set sharedProgramIds = formattedPrograms.stream().map(SharedOntology::getProgramId).collect(Collectors.toSet()); // Add other programs if (!sharedOnly) { @@ -65,7 +68,7 @@ public List getSharedOntology(@NotNull UUID programId, @NotNull Boolean sharedOn return formattedPrograms; } - private List getSharedProgramsFormatted(Program program) { + private List getSharedProgramsFormatted(Program program) { // Get shared ontology records List sharedOntologies = programOntologyDAO.getSharedOntologies(program.getId()); // Get the programs ontology is shared with @@ -98,16 +101,16 @@ private List getMatchingPrograms(Program program) { return matchingPrograms; } - private Program getProgram(UUID programId) { + private Program getProgram(UUID programId) throws DoesNotExistException { List programs = programDAO.get(programId); if (programs.size() == 0) { - throw new HttpStatusException(HttpStatus.NOT_FOUND, "Program with that id does not exist"); + throw new DoesNotExistException("Program with that id does not exist"); } return programs.get(0); } - private SharedProgram formatResponse(Program program, ProgramSharedOntologyEntity programSharedOntologyEntity, Boolean editable) { - return SharedProgram.builder() + private SharedOntology formatResponse(Program program, ProgramSharedOntologyEntity programSharedOntologyEntity, Boolean editable) { + return SharedOntology.builder() .programId(program.getId()) .programName(program.getName()) .shared(true) @@ -116,8 +119,8 @@ private SharedProgram formatResponse(Program program, ProgramSharedOntologyEntit .build(); } - private SharedProgram formatResponse(Program program) { - return SharedProgram.builder() + private SharedOntology formatResponse(Program program) { + return SharedOntology.builder() .programId(program.getId()) .programName(program.getName()) .shared(false) @@ -125,18 +128,21 @@ private SharedProgram formatResponse(Program program) { } private Boolean ontologyIsEditable(ProgramSharedOntologyEntity sharedOntologyEntity) { - if (sharedOntologyEntity.getActive()) { - /* TODO: Add in when checking for edits // Get all trait ids for the program - List traitIds = traitService.getByProgramId(sharedOntologyEntity.getSharedProgramId(), true).stream() + List traitIds = traitService.getSubscribedOntologyTraits(sharedOntologyEntity.getSharedProgramId()).stream() .map(trait -> trait.getId()) .collect(Collectors.toList()); + // Get the brapi program id + List program = programDAO.get(sharedOntologyEntity.getSharedProgramId()); + if (program.size() == 0) { + throw new InternalServerException("Missing program should have been caught by now"); + } + BrAPIProgram brAPIProgram = programDAO.getProgramBrAPI(program.get(0)); + // Get all observations for the ontology - return traitDAO.getObservationsForTraits(traitIds).isEmpty(); - */ - return true; + return traitDAO.getObservationsForTraitsByBrAPIProgram(brAPIProgram.getProgramDbId(), traitIds).isEmpty(); } else { return true; } @@ -151,7 +157,7 @@ private Boolean ontologyIsEditable(ProgramSharedOntologyEntity sharedOntologyEnt * @param programRequests -- List of programs to share ontology with * @return List */ - public List shareOntology(@NotNull UUID programId, AuthenticatedUser actingUser, List programRequests) throws ValidatorException, UnprocessableEntityException { + public List shareOntology(@NotNull UUID programId, AuthenticatedUser actingUser, List programRequests) throws ValidatorException, UnprocessableEntityException, DoesNotExistException { // Get program with that id Program program = getProgram(programId); @@ -164,7 +170,7 @@ public List shareOntology(@NotNull UUID programId, AuthenticatedU } // Don't allow shared if program is already subscribe to shared ontology - if (programOntologyDAO.programSubscribedSharedOntology(programId)) { + if (programOntologyDAO.getSubscribedSharedOntology(programId).isPresent()) { throw new UnprocessableEntityException("Program is subscribed to a shared ontology and cannot share its own."); } @@ -206,7 +212,7 @@ public List shareOntology(@NotNull UUID programId, AuthenticatedU // Query return data return getSharedProgramsFormatted(program).stream() - .filter(sharedProgram -> shareProgramIdsSet.contains(sharedProgram.getProgramId())) + .filter(sharedOntology -> shareProgramIdsSet.contains(sharedOntology.getProgramId())) .collect(Collectors.toList()); } @@ -234,4 +240,86 @@ public void revokeOntology(@NotNull UUID programId, @NotNull UUID sharedProgramI // Remove record from db programOntologyDAO.revokeSharedOntology(sharedOntology); } + + public SubscribedOntology subscribeOntology(UUID programId, UUID sharingProgramId) throws DoesNotExistException, UnprocessableEntityException { + // Check that program exists + Program program = getProgram(programId); + + // Check that shared program exists + Optional optionalSharedOntology = programOntologyDAO.getSharedOntologyById(sharingProgramId, programId); + if (optionalSharedOntology.isEmpty()) { + throw new DoesNotExistException("Shared ontology between specified programs was not found."); + } + ProgramSharedOntologyEntity sharedOntology = optionalSharedOntology.get(); + + // Check that program does not have any traits of its own + List traits = traitDAO.getTraitsByProgramId(programId); + if (traits.size() > 0) { + throw new UnprocessableEntityException("Program already has traits, cannot subscribe to a shared ontology"); + } + + // Check that program does not have any current shares of its own ontology + List sharedOntologies = programOntologyDAO.getSharedOntologies(program.getId()); + if (!sharedOntologies.isEmpty()) { + throw new UnprocessableEntityException("Program has shared its ontology with other programs, cannot subscribe to another ontology."); + } + + + // Subscribe + programOntologyDAO.acceptSharedOntology(sharedOntology); + + // Get the subscription record + try { + List subscribedOntologyOptions = getSubscribeOntologyOptions(programId); + for (SubscribedOntology option: subscribedOntologyOptions) { + if (option.getProgramId().equals(sharingProgramId)) { + return option; + } + } + } catch (DoesNotExistException e) { + throw new InternalServerException("Recently subscribed program cannot be found."); + } + + throw new InternalServerException("Recently subscribed program cannot be found."); + } + + public void unsubscribeOntology(UUID programId, UUID sharingProgramId) throws DoesNotExistException, UnprocessableEntityException { + // Check that program exists + Program program = getProgram(programId); + + // Check that shared program exists + Optional optionalSharedOntology = programOntologyDAO.getSharedOntologyById(sharingProgramId, programId); + if (optionalSharedOntology.isEmpty()) { + throw new DoesNotExistException("Shared ontology between specified programs was not found."); + } + ProgramSharedOntologyEntity sharedOntology = optionalSharedOntology.get(); + + if (!ontologyIsEditable(sharedOntology)) { + throw new UnprocessableEntityException("Shared program has recorded observations on shared traits and cannot unsubscribe."); + } + + // Subscribe + programOntologyDAO.denySharedOntology(sharedOntology); + } + + public List getSubscribeOntologyOptions(UUID programId) throws DoesNotExistException { + + Program program = getProgram(programId); + + List sharedOntologies = programOntologyDAO.getSubscriptionOptions(programId); + List programs = programDAO.get(sharedOntologies.stream().map(ProgramSharedOntologyEntity::getProgramId).collect(Collectors.toList())); + Map programMap = new HashMap<>(); + programs.forEach(sharedProgram -> programMap.put(sharedProgram.getId(), sharedProgram)); + + List subscriptionOptions = sharedOntologies.stream() + .map(sharedOntology -> SubscribedOntology.builder() + .programId(sharedOntology.getProgramId()) + .programName(programMap.get(sharedOntology.getProgramId()).getName()) + .subscribed(sharedOntology.getActive()) + .editable(sharedOntology.getActive() ? ontologyIsEditable(sharedOntology) : null) + .build() + ).collect(Collectors.toList()); + return subscriptionOptions; + } + } diff --git a/src/main/java/org/breedinginsight/services/TraitService.java b/src/main/java/org/breedinginsight/services/TraitService.java index 7fccf4351..2a8399e45 100644 --- a/src/main/java/org/breedinginsight/services/TraitService.java +++ b/src/main/java/org/breedinginsight/services/TraitService.java @@ -28,6 +28,7 @@ import org.breedinginsight.api.model.v1.response.ValidationErrors; import org.breedinginsight.dao.db.enums.DataType; import org.breedinginsight.dao.db.tables.pojos.MethodEntity; +import org.breedinginsight.dao.db.tables.pojos.ProgramSharedOntologyEntity; import org.breedinginsight.dao.db.tables.pojos.ScaleEntity; import org.breedinginsight.dao.db.tables.pojos.TraitEntity; import org.breedinginsight.daos.*; @@ -53,6 +54,7 @@ public class TraitService { private ObservationDAO observationDAO; private ProgramService programService; private ProgramOntologyService programOntologyService; + private ProgramOntologyDAO programOntologyDAO; private ProgramObservationLevelService programObservationLevelService; private UserService userService; private TraitValidatorService traitValidator; @@ -64,7 +66,8 @@ public class TraitService { @Inject public TraitService(TraitDAO traitDao, MethodDAO methodDao, ScaleDAO scaleDao, ObservationDAO observationDao, ProgramService programService, ProgramOntologyService programOntologyService, ProgramObservationLevelService programObservationLevelService, - UserService userService, TraitValidatorService traitValidator, DSLContext dsl, TraitValidatorError traitValidatorError) { + UserService userService, TraitValidatorService traitValidator, DSLContext dsl, TraitValidatorError traitValidatorError, + ProgramOntologyDAO programOntologyDAO) { this.traitDAO = traitDao; this.methodDAO = methodDao; this.scaleDAO = scaleDao; @@ -76,6 +79,7 @@ public TraitService(TraitDAO traitDao, MethodDAO methodDao, ScaleDAO scaleDao, O this.traitValidator = traitValidator; this.dsl = dsl; this.traitValidatorError = traitValidatorError; + this.programOntologyDAO = programOntologyDAO; } public List getByProgramId(UUID programId, boolean getFullTrait) throws DoesNotExistException { @@ -92,6 +96,16 @@ public List getByProgramId(UUID programId, boolean getFullTrait) throws D } + public List getSubscribedOntologyTraits(UUID programId) { + Optional optionalSharedOntology = programOntologyDAO.getSubscribedSharedOntology(programId); + if (optionalSharedOntology.isEmpty()) { + return new ArrayList<>(); + } + ProgramSharedOntologyEntity sharedOntology = optionalSharedOntology.get(); + + return traitDAO.getTraitsFullByProgramId(sharedOntology.getProgramId()); + } + public List getByProgramIds(List programIds, boolean getFullTrait) throws DoesNotExistException { if (programIds.stream().anyMatch(programId -> programService.exists(programId) == false)) { diff --git a/src/test/java/org/breedinginsight/TestUtils.java b/src/test/java/org/breedinginsight/TestUtils.java index ad1dc79b1..effb88082 100644 --- a/src/test/java/org/breedinginsight/TestUtils.java +++ b/src/test/java/org/breedinginsight/TestUtils.java @@ -28,6 +28,7 @@ import org.breedinginsight.api.model.v1.request.ProgramRequest; import org.breedinginsight.api.v1.controller.metadata.SortOrder; import org.breedinginsight.model.Program; +import org.breedinginsight.model.Trait; import se.sawano.java.text.AlphanumericComparator; import java.io.File; @@ -219,4 +220,17 @@ public static HttpResponse getUploadedFile(RxHttpClient client, UUID pro return response; } } + + public static void insertTestTraits(Gson gson, RxHttpClient client, Program program, List traits) { + + String url = String.format("/programs/%s/traits", program.getId()); + String json = gson.toJson(traits); + Flowable> call = client.exchange( + POST(url, json) + .contentType(MediaType.APPLICATION_JSON) + .cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + + HttpResponse response = call.blockingFirst(); + } } diff --git a/src/test/java/org/breedinginsight/api/v1/controller/OntologyControllerIntegrationTest.java b/src/test/java/org/breedinginsight/api/v1/controller/OntologyControllerIntegrationTest.java index 7228822db..82195581c 100644 --- a/src/test/java/org/breedinginsight/api/v1/controller/OntologyControllerIntegrationTest.java +++ b/src/test/java/org/breedinginsight/api/v1/controller/OntologyControllerIntegrationTest.java @@ -11,10 +11,7 @@ import io.micronaut.http.netty.cookies.NettyCookie; import io.micronaut.test.annotation.MicronautTest; import io.reactivex.Flowable; -import org.brapi.v2.model.pheno.BrAPIObservation; -import org.brapi.v2.model.pheno.BrAPIObservationVariable; import org.breedinginsight.BrAPITest; -import org.breedinginsight.DatabaseTest; import org.breedinginsight.TestUtils; import org.breedinginsight.api.model.v1.request.ProgramRequest; import org.breedinginsight.api.model.v1.request.SharedOntologyProgramRequest; @@ -33,7 +30,6 @@ import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.UUID; import static io.micronaut.http.HttpRequest.*; @@ -50,6 +46,7 @@ public class OntologyControllerIntegrationTest extends BrAPITest { private Program mainProgram; private Program otherProgram; private Program thirdProgram; + private Program fourthProgram; @Inject private DSLContext dsl; @@ -111,24 +108,35 @@ void setup() throws Exception { .species(speciesRequest) .key("OTC") .build(); + ProgramRequest programRequest4 = ProgramRequest.builder() + .name("Test Program4") + .abbreviation("test3") + .documentationUrl("localhost:8080") + .objective("To test things") + .species(speciesRequest) + .key("OTD") + .build(); + TestUtils.insertAndFetchTestProgram(gson, client, programRequest1); TestUtils.insertAndFetchTestProgram(gson, client, programRequest2); TestUtils.insertAndFetchTestProgram(gson, client, programRequest3); + TestUtils.insertAndFetchTestProgram(gson, client, programRequest4); // Get main program List programs = programDAO.getAll(); mainProgram = programs.get(0); otherProgram = programs.get(1); thirdProgram = programs.get(2); + fourthProgram = programs.get(3); dsl.execute(securityFp.get("InsertProgramRolesBreeder"), testUser.getId().toString(), mainProgram.getId().toString()); dsl.execute(securityFp.get("InsertProgramRolesBreeder"), testUser.getId().toString(), otherProgram.getId().toString()); + dsl.execute(securityFp.get("InsertProgramRolesBreeder"), testUser.getId().toString(), thirdProgram.getId().toString()); // Add trait to program - addTrait(otherProgram); - // Add a single observation to all traits - super.getBrapiDsl().execute(brapiObservationFp.get("AddObservations")); + addTrait(mainProgram); + addTrait(thirdProgram); } private void addTrait(Program program) { @@ -155,13 +163,7 @@ private void addTrait(Program program) { List traits = List.of(trait); // Call endpoint - Flowable> call = client.exchange( - POST("/programs/" + program.getId() + "/traits", traits) - .contentType(MediaType.APPLICATION_JSON) - .cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class - ); - HttpResponse response = call.blockingFirst(); - assertEquals(HttpStatus.OK, response.getStatus()); + TestUtils.insertTestTraits(gson, client, program, traits); } @Test @@ -177,7 +179,7 @@ void getAllProgramsNoSharedPrograms() { JsonObject result = JsonParser.parseString(response.body()).getAsJsonObject().getAsJsonObject("result"); JsonArray data = result.getAsJsonArray("data"); - assertEquals(2, data.size(), "Wrong number of programs returned"); + assertEquals(3, data.size(), "Wrong number of programs returned"); // Check all are not shared and are inactive for (JsonElement element: data) { @@ -196,6 +198,7 @@ void addSharedPrograms() { String url = String.format("/programs/%s/ontology/shared/programs", mainProgram.getId()); List requests = new ArrayList<>(); requests.add(new SharedOntologyProgramRequest(otherProgram.getId(), otherProgram.getName())); + requests.add(new SharedOntologyProgramRequest(thirdProgram.getId(), thirdProgram.getName())); String json = gson.toJson(requests); Flowable> call = client.exchange( @@ -209,7 +212,7 @@ void addSharedPrograms() { JsonObject result = JsonParser.parseString(response.body()).getAsJsonObject().getAsJsonObject("result"); JsonArray data = result.getAsJsonArray("data"); - assertEquals(1, data.size(), "Wrong number of programs returned"); + assertEquals(2, data.size(), "Wrong number of programs returned"); // Check all are not shared and are inactive for (JsonElement element: data) { @@ -222,14 +225,65 @@ void addSharedPrograms() { } @Test - @Order(2) - void shareOntologySubscribedToOtherProgram() { - // TODO: When subscribe ontology card is done - // Cannot share ontology if you are using a shared ontology + @Order(3) + void subscribeOntologyProgramHasTraitsError() { + String url = String.format("/programs/%s/ontology/subscribe/%s", thirdProgram.getId(), mainProgram.getId()); + + Flowable> call = client.exchange( + PUT(url, "") + .contentType(MediaType.APPLICATION_JSON) + .cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + + HttpClientResponseException e = Assertions.assertThrows(HttpClientResponseException.class, () -> { + HttpResponse response = call.blockingFirst(); + }); + + assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, e.getStatus()); } @Test @Order(3) + void subscribeOntologySuccess() { + String url = String.format("/programs/%s/ontology/subscribe/%s", otherProgram.getId(), mainProgram.getId()); + + Flowable> call = client.exchange( + PUT(url, "") + .contentType(MediaType.APPLICATION_JSON) + .cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + + HttpResponse response = call.blockingFirst(); + assertEquals(HttpStatus.OK, response.getStatus()); + } + + @Test + @Order(4) + void getSubscribedOntologyOptions() { + + String url = String.format("/programs/%s/ontology/subscribe", otherProgram.getId()); + Flowable> call = client.exchange( + GET(url).cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + + HttpResponse response = call.blockingFirst(); + assertEquals(HttpStatus.OK, response.getStatus()); + + JsonObject result = JsonParser.parseString(response.body()).getAsJsonObject().getAsJsonObject("result"); + JsonArray data = result.getAsJsonArray("data"); + assertEquals(1, data.size(), "Wrong number of programs returned"); + + // Check all are not shared and are inactive + for (JsonElement element: data) { + JsonObject program = element.getAsJsonObject(); + + assertTrue(program.get("subscribed").getAsBoolean()); + assertTrue(program.get("editable").getAsBoolean()); + } + } + + @Test + @Order(4) void getAllProgramsSharedPrograms() { String url = String.format("/programs/%s/ontology/shared/programs", mainProgram.getId()); Flowable> call = client.exchange( @@ -241,27 +295,28 @@ void getAllProgramsSharedPrograms() { JsonObject result = JsonParser.parseString(response.body()).getAsJsonObject().getAsJsonObject("result"); JsonArray data = result.getAsJsonArray("data"); - assertEquals(2, data.size(), "Wrong number of programs returned"); + assertEquals(3, data.size(), "Wrong number of programs returned"); // Check all are not shared and are inactive for (JsonElement element: data) { JsonObject program = element.getAsJsonObject(); - if (program.get("programId").getAsString().equals(otherProgram.getId().toString())) { + if (program.get("programId").getAsString().equals(fourthProgram.getId().toString())) { + assertFalse(program.get("shared").getAsBoolean()); + } else { assertTrue(program.get("shared").getAsBoolean(), "Shared should have been true"); - assertFalse(program.get("accepted").getAsBoolean(), "Accepted should have been false"); assertTrue(program.get("editable").getAsBoolean(), "Editable should have been false"); - } else { - assertFalse(program.get("shared").getAsBoolean(), "Shared should have been false"); - assertNull(program.get("accepted"), "Accepted should have been false"); - assertNull(program.get("editable"), "Editable should have been false"); + if (program.get("programId").getAsString().equals(otherProgram.getId().toString())) { + assertTrue(program.get("accepted").getAsBoolean()); + } else { + assertFalse(program.get("accepted").getAsBoolean()); + } } - } } @Test - @Order(3) + @Order(4) void getOnlySharedPrograms() { String url = String.format("/programs/%s/ontology/shared/programs?shared=true", mainProgram.getId()); Flowable> call = client.exchange( @@ -273,30 +328,92 @@ void getOnlySharedPrograms() { JsonObject result = JsonParser.parseString(response.body()).getAsJsonObject().getAsJsonObject("result"); JsonArray data = result.getAsJsonArray("data"); - assertEquals(1, data.size(), "Wrong number of programs returned"); + assertEquals(2, data.size(), "Wrong number of programs returned"); // Check all are not shared and are inactive for (JsonElement element: data) { JsonObject program = element.getAsJsonObject(); assertTrue(program.get("shared").getAsBoolean(), "Shared should have been true"); - assertFalse(program.get("accepted").getAsBoolean(), "Accepted should have been false"); - assertTrue(program.get("editable").getAsBoolean(), "Editable should have been false"); } } + @Test + @Order(4) + void shareOntologySubscribedToOtherProgram() { + // Cannot share ontology if you are using a shared ontology + String url = String.format("/programs/%s/ontology/shared/programs", otherProgram.getId()); + List requests = new ArrayList<>(); + requests.add(new SharedOntologyProgramRequest(fourthProgram.getId(), fourthProgram.getName())); + String json = gson.toJson(requests); + + Flowable> call = client.exchange( + POST(url, json) + .contentType(MediaType.APPLICATION_JSON) + .cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + + HttpClientResponseException e = Assertions.assertThrows(HttpClientResponseException.class, () -> { + HttpResponse response = call.blockingFirst(); + }); + assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, e.getStatus()); + } + @Test @Order(4) void revokeOntologyUneditable() { - // TODO: When subscribe ontology card is done // Ontology cannot be revoke if shared program has accepted and has observations + super.getBrapiDsl().execute(brapiObservationFp.get("AddObservations"), otherProgram.getId().toString()); + + String url = String.format("/programs/%s/ontology/shared/programs/%s", mainProgram.getId(), otherProgram.getId()); + Flowable> call = client.exchange( + DELETE(url).cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + + HttpClientResponseException e = Assertions.assertThrows(HttpClientResponseException.class, () -> { + HttpResponse response = call.blockingFirst(); + }); + assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, e.getStatus()); + + super.getBrapiDsl().execute(brapiObservationFp.get("DeleteObservations")); + } + + @Test + @Order(4) + void unsubscribeOntologyUneditableError() { + super.getBrapiDsl().execute(brapiObservationFp.get("AddObservations"), otherProgram.getId().toString()); + + String url = String.format("/programs/%s/ontology/subscribe/%s", otherProgram.getId(), mainProgram.getId()); + Flowable> call = client.exchange( + DELETE(url, "").cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + + HttpClientResponseException e = Assertions.assertThrows(HttpClientResponseException.class, () -> { + HttpResponse response = call.blockingFirst(); + }); + assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, e.getStatus()); + + super.getBrapiDsl().execute(brapiObservationFp.get("DeleteObservations")); } @Test @Order(5) - void revokeProgram() { + void unsubscribeOntologySuccess() { - String url = String.format("/programs/%s/ontology/shared/programs/%s", mainProgram.getId(), otherProgram.getId()); + String url = String.format("/programs/%s/ontology/subscribe/%s", otherProgram.getId(), mainProgram.getId()); + Flowable> call = client.exchange( + DELETE(url, "").cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + + HttpResponse response = call.blockingFirst(); + assertEquals(HttpStatus.OK, response.getStatus()); + } + + @Test + @Order(5) + void revokeOntologySuccess() { + + String url = String.format("/programs/%s/ontology/shared/programs/%s", mainProgram.getId(), thirdProgram.getId()); Flowable> call = client.exchange( DELETE(url).cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class ); @@ -354,7 +471,7 @@ void revokeOntologySharedProgramNotExist() { @Test void revokeOntologySharedProgramNotShared() { - String url = String.format("/programs/%s/ontology/shared/programs/%s", mainProgram.getId(), thirdProgram.getId()); + String url = String.format("/programs/%s/ontology/shared/programs/%s", mainProgram.getId(), fourthProgram.getId()); Flowable> call = client.exchange( DELETE(url).cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class ); diff --git a/src/test/resources/sql/brapi/BrAPIOntologyControllerIntegrationTest.sql b/src/test/resources/sql/brapi/BrAPIOntologyControllerIntegrationTest.sql index df07c0dfa..676f7055b 100644 --- a/src/test/resources/sql/brapi/BrAPIOntologyControllerIntegrationTest.sql +++ b/src/test/resources/sql/brapi/BrAPIOntologyControllerIntegrationTest.sql @@ -17,5 +17,20 @@ */ -- name: AddObservations -insert into observation (id, observation_variable_id, value) -select row_number() over (order by id), id, 'test' from observation_variable; \ No newline at end of file +insert into observation (id, observation_variable_id, program_id, value) +select md5(random()::text || clock_timestamp()::text)::uuid, observation_variable.id, matching_program.id, 'test' +from observation_variable +join + ( + select p.id, er.external_reference_id from + "program" p + join + program_external_references per on p.id = per.program_entity_id + join + external_reference er on per.external_references_id = er.id + where + er.external_reference_id = ?::text + ) as matching_program on 1=1; + +-- name: DeleteObservations +delete from observation; \ No newline at end of file