From f6a8cd904f135322ecd9cc2354b0d499fe0ed072 Mon Sep 17 00:00:00 2001 From: Chris T Date: Mon, 14 Mar 2022 13:34:47 -0400 Subject: [PATCH 1/9] shared ontology: POST and GET endpoints --- .../request/SharedOntologyProgramRequest.java | 20 ++ .../api/v1/controller/OntologyController.java | 93 ++++++++++ .../org/breedinginsight/daos/ProgramDAO.java | 7 +- .../daos/ProgramOntologyDAO.java | 50 ++++- .../breedinginsight/model/BrAPIConstants.java | 18 ++ .../org/breedinginsight/model/Program.java | 31 ++-- .../breedinginsight/model/SharedProgram.java | 17 ++ .../services/OntologyService.java | 172 ++++++++++++++++++ ...0.5.33__created_shared_ontology_tables.sql | 30 +++ .../OntologyControllerIntegrationTest.java | 54 ++++++ 10 files changed, 473 insertions(+), 19 deletions(-) create mode 100644 src/main/java/org/breedinginsight/api/model/v1/request/SharedOntologyProgramRequest.java create mode 100644 src/main/java/org/breedinginsight/api/v1/controller/OntologyController.java create mode 100644 src/main/java/org/breedinginsight/model/BrAPIConstants.java create mode 100644 src/main/java/org/breedinginsight/model/SharedProgram.java create mode 100644 src/main/java/org/breedinginsight/services/OntologyService.java create mode 100644 src/main/resources/db/migration/V0.5.33__created_shared_ontology_tables.sql create mode 100644 src/test/java/org/breedinginsight/api/v1/controller/OntologyControllerIntegrationTest.java diff --git a/src/main/java/org/breedinginsight/api/model/v1/request/SharedOntologyProgramRequest.java b/src/main/java/org/breedinginsight/api/model/v1/request/SharedOntologyProgramRequest.java new file mode 100644 index 000000000..b23902cbd --- /dev/null +++ b/src/main/java/org/breedinginsight/api/model/v1/request/SharedOntologyProgramRequest.java @@ -0,0 +1,20 @@ +package org.breedinginsight.api.model.v1.request; + +import io.micronaut.core.annotation.Introspected; +import lombok.*; + +import javax.validation.constraints.NotBlank; +import java.util.UUID; + +@Getter +@Setter +@Builder +@ToString +@AllArgsConstructor +@NoArgsConstructor +@Introspected +public class SharedOntologyProgramRequest { + @NotBlank + private UUID id; + private String programName; +} diff --git a/src/main/java/org/breedinginsight/api/v1/controller/OntologyController.java b/src/main/java/org/breedinginsight/api/v1/controller/OntologyController.java new file mode 100644 index 000000000..9f9fed262 --- /dev/null +++ b/src/main/java/org/breedinginsight/api/v1/controller/OntologyController.java @@ -0,0 +1,93 @@ +package org.breedinginsight.api.v1.controller; + +import io.micronaut.http.HttpResponse; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.*; +import org.breedinginsight.api.auth.ProgramSecured; +import org.breedinginsight.api.auth.ProgramSecuredRoleGroup; +import org.breedinginsight.api.auth.SecurityService; +import org.breedinginsight.api.model.v1.response.DataResponse; +import org.breedinginsight.api.model.v1.response.Response; +import org.breedinginsight.model.Trait; +import org.breedinginsight.services.OntologyService; + +import javax.inject.Inject; +import javax.ws.rs.QueryParam; +import java.util.UUID; + +public class OntologyController { + + private SecurityService securityService; + private OntologyService ontologyService; + + @Inject + private OntologyController(SecurityService securityService, OntologyService ontologyService) { + this.securityService = securityService; + this.ontologyService = ontologyService; + } + + /** + * Returns all available programs available to share programs ontology with. Includes programs + * currently being shared with and their editable (unshareable) status. + * + * @param programId + * @param shared + * @return + * { + * data: [ + * { + * name: string, -- Program name + * id: UUID, -- Program ID + * shared: true, + * status: ACCEPTED | PENDING + * } + * ] + * } + */ + @Get("/programs/{programId}/ontology/shared/programs{?shared}") + @Produces(MediaType.APPLICATION_JSON) + @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL}) + public HttpResponse>> getAvailablePrograms( + @PathVariable UUID programId, @QueryValue(defaultValue = "false") Boolean shared) { + return HttpResponse.ok(); + } + + /** + * Accepts a list of programs to shared the ontology with. + * @param programId + * @return List of programs successfully shared to with acceptable status + * { + * data: [ + * { + * name: string, -- Program name + * id: UUID, -- Program ID + * shared: true, + * status: ACCEPTED | PENDING + * } + * ] + * } + */ + @Post("/programs/{programId}/ontology/shared/programs") + @Produces(MediaType.APPLICATION_JSON) + @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL}) + public HttpResponse>> shareOntology( + @PathVariable UUID programId) { + return HttpResponse.ok(); + } + + /** + * Revokes access to shared ontology from a program. If program is not currently shared with + * will still return a 200. + * + * @param programId + * @param sharedProgramId + * @return + */ + @Delete("/programs/{programId}/ontology/shared/programs/{sharedProgramId}") + @Produces(MediaType.APPLICATION_JSON) + @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL}) + public HttpResponse>> revokeOntology( + @PathVariable UUID programId, @PathVariable UUID sharedProgramId) { + return HttpResponse.ok(); + } +} diff --git a/src/main/java/org/breedinginsight/daos/ProgramDAO.java b/src/main/java/org/breedinginsight/daos/ProgramDAO.java index c7835fd2d..0cf0845f6 100644 --- a/src/main/java/org/breedinginsight/daos/ProgramDAO.java +++ b/src/main/java/org/breedinginsight/daos/ProgramDAO.java @@ -36,9 +36,7 @@ import org.breedinginsight.dao.db.tables.BiUserTable; import org.breedinginsight.dao.db.tables.daos.ProgramDao; import org.breedinginsight.dao.db.tables.pojos.ProgramEntity; -import org.breedinginsight.model.Program; -import org.breedinginsight.model.ProgramBrAPIEndpoints; -import org.breedinginsight.model.Species; +import org.breedinginsight.model.*; import org.breedinginsight.model.User; import org.breedinginsight.services.brapi.BrAPIClientProvider; import org.breedinginsight.services.brapi.BrAPIClientType; @@ -76,7 +74,7 @@ public class ProgramDAO extends ProgramDao { private String referenceSource; private Duration requestTimeout; - private final static String SYSTEM_DEFAULT = "System Default"; + private final static String SYSTEM_DEFAULT = BrAPIConstants.SYSTEM_DEFAULT.getValue(); @Inject public ProgramDAO(Configuration config, DSLContext dsl, BrAPIProvider brAPIProvider, BrAPIClientProvider brAPIClientProvider, @@ -364,6 +362,5 @@ private Duration getRequestTimeout() { return Duration.of(5, ChronoUnit.MINUTES); } - } diff --git a/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java b/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java index 16a48732a..46a7e27bb 100644 --- a/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java +++ b/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java @@ -17,19 +17,67 @@ package org.breedinginsight.daos; +import org.breedinginsight.dao.db.tables.BiUserTable; +import org.breedinginsight.dao.db.tables.ProgramTable; import org.breedinginsight.dao.db.tables.daos.ProgramOntologyDao; +import org.breedinginsight.dao.db.tables.daos.ProgramSharedOntologyDao; +import org.breedinginsight.dao.db.tables.pojos.ProgramSharedOntologyEntity; +import org.breedinginsight.model.BrAPIConstants; +import org.breedinginsight.model.Program; +import org.breedinginsight.model.User; import org.jooq.Configuration; import org.jooq.DSLContext; +import org.jooq.Record; +import org.jooq.Result; import javax.inject.Inject; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.breedinginsight.dao.db.Tables.*; public class ProgramOntologyDAO extends ProgramOntologyDao { private DSLContext dsl; + private ProgramSharedOntologyDao programSharedOntologyDao; @Inject - public ProgramOntologyDAO(Configuration config, DSLContext dsl) { + public ProgramOntologyDAO(Configuration config, DSLContext dsl, ProgramSharedOntologyDao programSharedOntologyDao) { super(config); this.dsl = dsl; + this.programSharedOntologyDao = programSharedOntologyDao; + } + + /** + * Retrieves all programs that the given program has shared its ontology with. + * @param programId + */ + public List getSharedPrograms(UUID programId) { + + ProgramTable sharedProgram = PROGRAM.as("sharedProgram"); + + Result queryResult = dsl.select() + .from(PROGRAM) + .join(PROGRAM_SHARED_ONTOLOGY) + .on(PROGRAM.ID.eq(PROGRAM_SHARED_ONTOLOGY.PROGRAM_ID)) + .join(sharedProgram) + .on(PROGRAM_SHARED_ONTOLOGY.SHARED_PROGRAM_ID.eq(sharedProgram.ID)) + .where(PROGRAM.ID.eq(programId)) + .fetch(); + + List sharedPrograms = new ArrayList<>(); + for (Record record: queryResult){ + if (record.getValue(PROGRAM.BRAPI_URL) == null) { + record.setValue(PROGRAM.BRAPI_URL, BrAPIConstants.SYSTEM_DEFAULT.getValue()); + } + Program program = Program.parseSQLRecord(record); + sharedPrograms.add(program); + } + return sharedPrograms; + } + + public void createSharedOntologies(List shareRecords) { + programSharedOntologyDao.insert(shareRecords); } } diff --git a/src/main/java/org/breedinginsight/model/BrAPIConstants.java b/src/main/java/org/breedinginsight/model/BrAPIConstants.java new file mode 100644 index 000000000..47ba71804 --- /dev/null +++ b/src/main/java/org/breedinginsight/model/BrAPIConstants.java @@ -0,0 +1,18 @@ +package org.breedinginsight.model; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum BrAPIConstants { + SYSTEM_DEFAULT("System Default"); + + private String value; + + BrAPIConstants(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/src/main/java/org/breedinginsight/model/Program.java b/src/main/java/org/breedinginsight/model/Program.java index 22616e58e..35cebadb5 100644 --- a/src/main/java/org/breedinginsight/model/Program.java +++ b/src/main/java/org/breedinginsight/model/Program.java @@ -24,6 +24,7 @@ import lombok.experimental.SuperBuilder; import org.brapi.v2.model.core.BrAPIProgram; import org.brapi.v2.model.pheno.BrAPIObservationVariable; +import org.breedinginsight.dao.db.tables.ProgramTable; import org.breedinginsight.dao.db.tables.pojos.ProgramEntity; import org.breedinginsight.dao.db.tables.pojos.SpeciesEntity; import org.jooq.Record; @@ -65,22 +66,26 @@ public Program(ProgramEntity programEntity){ } public static Program parseSQLRecord(Record record){ + return parseSQLRecord(record, PROGRAM); + } + + public static Program parseSQLRecord(Record record, ProgramTable programTable) { // Generate our program record Program program = Program.builder() - .id(record.getValue(PROGRAM.ID)) - .name(record.getValue(PROGRAM.NAME)) - .abbreviation(record.getValue(PROGRAM.ABBREVIATION)) - .objective(record.getValue(PROGRAM.OBJECTIVE)) - .documentationUrl(record.getValue(PROGRAM.DOCUMENTATION_URL)) - .brapiUrl(record.getValue(PROGRAM.BRAPI_URL)) - .key(record.getValue(PROGRAM.KEY)) - .createdAt(record.getValue(PROGRAM.CREATED_AT)) - .updatedAt(record.getValue(PROGRAM.UPDATED_AT)) - .createdBy(record.getValue(PROGRAM.CREATED_BY)) - .updatedBy(record.getValue(PROGRAM.UPDATED_BY)) - .active(record.getValue(PROGRAM.ACTIVE)) - .germplasmSequence(record.getValue(PROGRAM.GERMPLASM_SEQUENCE)) + .id(record.getValue(programTable.ID)) + .name(record.getValue(programTable.NAME)) + .abbreviation(record.getValue(programTable.ABBREVIATION)) + .objective(record.getValue(programTable.OBJECTIVE)) + .documentationUrl(record.getValue(programTable.DOCUMENTATION_URL)) + .brapiUrl(record.getValue(programTable.BRAPI_URL)) + .key(record.getValue(programTable.KEY)) + .createdAt(record.getValue(programTable.CREATED_AT)) + .updatedAt(record.getValue(programTable.UPDATED_AT)) + .createdBy(record.getValue(programTable.CREATED_BY)) + .updatedBy(record.getValue(programTable.UPDATED_BY)) + .active(record.getValue(programTable.ACTIVE)) + .germplasmSequence(record.getValue(programTable.GERMPLASM_SEQUENCE)) .build(); return program; diff --git a/src/main/java/org/breedinginsight/model/SharedProgram.java b/src/main/java/org/breedinginsight/model/SharedProgram.java new file mode 100644 index 000000000..9c11fb992 --- /dev/null +++ b/src/main/java/org/breedinginsight/model/SharedProgram.java @@ -0,0 +1,17 @@ +package org.breedinginsight.model; + +import lombok.*; + +import java.util.UUID; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SharedProgram { + private UUID program_id; + private String program_name; + private Boolean shared; + private Boolean editable; +} diff --git a/src/main/java/org/breedinginsight/services/OntologyService.java b/src/main/java/org/breedinginsight/services/OntologyService.java new file mode 100644 index 000000000..938ece75b --- /dev/null +++ b/src/main/java/org/breedinginsight/services/OntologyService.java @@ -0,0 +1,172 @@ +package org.breedinginsight.services; + +import io.micronaut.http.HttpStatus; +import io.micronaut.http.exceptions.HttpStatusException; +import org.breedinginsight.api.auth.AuthenticatedUser; +import org.breedinginsight.api.model.v1.request.SharedOntologyProgramRequest; +import org.breedinginsight.api.model.v1.response.ValidationError; +import org.breedinginsight.api.model.v1.response.ValidationErrors; +import org.breedinginsight.dao.db.tables.pojos.ProgramSharedOntologyEntity; +import org.breedinginsight.daos.ProgramDAO; +import org.breedinginsight.daos.ProgramOntologyDAO; +import org.breedinginsight.model.Program; +import org.breedinginsight.model.SharedProgram; +import org.breedinginsight.services.exceptions.UnprocessableEntityException; +import org.breedinginsight.services.exceptions.ValidatorException; + +import javax.inject.Singleton; +import javax.validation.constraints.NotNull; +import java.util.*; +import java.util.stream.Collectors; + +@Singleton +public class OntologyService { + + private ProgramDAO programDAO; + private ProgramOntologyDAO programOntologyDAO; + + public OntologyService(ProgramDAO programDAO, ProgramOntologyDAO programOntologyDAO) { + this.programDAO = programDAO; + this.programOntologyDAO = programOntologyDAO; + } + + /** + * Gets programs available to share a programs ontology with. + * @param programId -- Program that owns the ontology + * @param sharedOnly -- True = return only shared programs, False = get all shareable programs + * @return List + */ + public List getSharedOntology(@NotNull UUID programId, @NotNull Boolean sharedOnly) { + + // Get program with that id + Program program = getProgram(programId); + + // Get programs of same species and brapi server from db + // TODO: Test if localhost vs localhost/brapi/v2 makes a difference + List matchingPrograms = getMatchingPrograms(program); + + // Get shared ontology programs for db + List sharedPrograms = programOntologyDAO.getSharedPrograms(program.getId()); + + // Format response + // Store shared programs in map for lookup + Map sharedProgramsMap = new HashMap<>(); + sharedPrograms.stream().forEach(sharedProgram -> sharedProgramsMap.put(sharedProgram.getId(), sharedProgram)); + + // Loop through or matching programs, formatting and labeling shared ones. + // TODO: Need to check if these programs have observations + List formattedPrograms = new ArrayList<>(); + for (Program matchingProgram: matchingPrograms) { + SharedProgram formattedProgram = formatResponse(matchingProgram, + sharedProgramsMap.containsKey(matchingProgram.getId()), false); + + formattedPrograms.add(formattedProgram); + } + + + return formattedPrograms; + } + + private List getMatchingPrograms(Program program) { + List allPrograms = programDAO.getAll(); + List matchingPrograms = new ArrayList<>(); + for (Program candidate: allPrograms) { + + if (candidate.getSpecies().getId().equals(program.getSpecies().getId()) && + candidate.getBrapiUrl().equals(program.getBrapiUrl())) { + + matchingPrograms.add(candidate); + } + } + return matchingPrograms; + } + + private Program getProgram(UUID programId) { + List programs = programDAO.get(programId); + if (programs.size() == 0) { + throw new HttpStatusException(HttpStatus.NOT_FOUND, "Program with that id does not exist"); + } + return programs.get(0); + } + + private SharedProgram formatResponse(Program program, Boolean shared, Boolean editable) { + return SharedProgram.builder() + .program_id(program.getId()) + .program_name(program.getName()) + .shared(shared) + .build(); + } + + + /** + * Processes share requests for a list of programs. Will return a ValidationError if there are issues + * with any of the shared requests. + * + * @param programId -- Program that owns the ontology + * @param programRequests -- List of programs to share ontology with + * @return Lst + */ + public List shareOntology(@NotNull UUID programId, AuthenticatedUser actingUser, List programRequests) throws ValidatorException { + // Get program with that id + Program program = getProgram(programId); + + // Check shareability, same brapi server, same species + List matchingPrograms = getMatchingPrograms(program); + Set matchingProgramsSet = new HashSet<>(); + matchingPrograms.stream().forEach(matchingProgram -> matchingProgramsSet.add(matchingProgram.getId())); + + Set shareProgramIdsSet = new HashSet<>(); + ValidationErrors validationErrors = new ValidationErrors(); + for (int i = 0; i < programRequests.size(); i++) { + SharedOntologyProgramRequest programRequest = programRequests.get(i); + if (!matchingProgramsSet.contains(programRequest.getId())) { + ValidationError error = new ValidationError("program", + "Program does not have same species or brapi server.", + HttpStatus.UNPROCESSABLE_ENTITY); + validationErrors.addError(i, error); + } else { + shareProgramIdsSet.add(programRequest.getId()); + } + } + + if (validationErrors.hasErrors()) { + throw new ValidatorException(validationErrors); + } + + // Add shared record to DB + List shareRecords = new ArrayList<>(); + for (UUID shareProgramId: shareProgramIdsSet){ + ProgramSharedOntologyEntity programSharedOntologyEntity = ProgramSharedOntologyEntity.builder() + .programId(programId) + .sharedProgramId(shareProgramId) + .updatedBy(actingUser.getId()) + .createdBy(actingUser.getId()) + .build(); + shareRecords.add(programSharedOntologyEntity); + } + programOntologyDAO.createSharedOntologies(shareRecords); + + // Query return data + List allSharedPrograms = programOntologyDAO.getSharedPrograms(programId); + List newSharedPrograms = allSharedPrograms.stream() + .filter(sharedProgram -> shareProgramIdsSet.contains(sharedProgram.getId())) + .collect(Collectors.toList()); + List sharedPrograms = newSharedPrograms.stream().map( + newSharedProgram -> formatResponse(newSharedProgram, false, true) + ).collect(Collectors.toList()); + + return sharedPrograms; + } + + /** + * Removes ontology sharing from the specific program. + * @param programId -- Program that owns the ontology. + * @param sharedProgramId -- Program to revoke shared ontology access from + */ + public void revokeOntology(@NotNull UUID programId, @NotNull UUID sharedProgramId) { + // TODO: Check that shared program is still unshareable. No observations yet. + + // TODO: Remove record from db + + } +} diff --git a/src/main/resources/db/migration/V0.5.33__created_shared_ontology_tables.sql b/src/main/resources/db/migration/V0.5.33__created_shared_ontology_tables.sql new file mode 100644 index 000000000..abaa1b8fb --- /dev/null +++ b/src/main/resources/db/migration/V0.5.33__created_shared_ontology_tables.sql @@ -0,0 +1,30 @@ +/* + * 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. + */ + + CREATE TABLE program_shared_ontology ( + like base_entity INCLUDING DEFAULTS INCLUDING CONSTRAINTS INCLUDING INDEXES, + program_id UUID, + shared_program_id UUID, + active boolean NOT NULL DEFAULT false, + shared_on timestamptz(0) NOT NULL default now(), + like base_edit_track_entity INCLUDING DEFAULTS INCLUDING CONSTRAINTS INCLUDING INDEXES + ); + +ALTER TABLE program_shared_ontology ADD FOREIGN KEY (created_by) REFERENCES bi_user (id); +ALTER TABLE program_shared_ontology ADD FOREIGN KEY (updated_by) REFERENCES bi_user (id); +ALTER TABLE program_shared_ontology ADD FOREIGN KEY (program_id) REFERENCES program (id); +ALTER TABLE program_shared_ontology ADD FOREIGN KEY (shared_program_id) REFERENCES program (id); \ No newline at end of file diff --git a/src/test/java/org/breedinginsight/api/v1/controller/OntologyControllerIntegrationTest.java b/src/test/java/org/breedinginsight/api/v1/controller/OntologyControllerIntegrationTest.java new file mode 100644 index 000000000..bbbf82399 --- /dev/null +++ b/src/test/java/org/breedinginsight/api/v1/controller/OntologyControllerIntegrationTest.java @@ -0,0 +1,54 @@ +package org.breedinginsight.api.v1.controller; + +import io.kowalski.fannypack.FannyPack; +import io.micronaut.http.client.RxHttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.test.annotation.MicronautTest; +import org.breedinginsight.BrAPITest; +import org.junit.jupiter.api.*; + +import javax.inject.Inject; + +@MicronautTest +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class OntologyControllerIntegrationTest extends BrAPITest { + + private FannyPack fp; + + @Inject + @Client("/${micronaut.bi.api.version}") + private RxHttpClient client; + + @AfterAll + public void finish() { super.stopContainers(); } + + @BeforeAll + void setup() throws Exception { + // Create two programs with fanny pack + } + + @Test + @Order(1) + void getAllProgramsNoPrograms() { + + } + + @Test + @Order(2) + void addSharedPrograms() { + + } + + @Test + @Order(3) + void getAllProgramsSharedPrograms() { + + } + + @Test + @Order(4) + void revokeProgram() { + + } +} From 0c20f083283f6e5c9cfea9031bf2902081aa429e Mon Sep 17 00:00:00 2001 From: Chris T Date: Mon, 21 Mar 2022 10:45:55 -0400 Subject: [PATCH 2/9] post and get shared ontologies: tests and endpoints complete --- .../api/v1/controller/OntologyController.java | 53 +++- .../daos/ProgramOntologyDAO.java | 6 + .../breedinginsight/model/SharedProgram.java | 5 +- .../services/OntologyService.java | 105 +++++--- .../OntologyControllerIntegrationTest.java | 255 +++++++++++++++++- .../sql/OntologyControllerIntegrationTest.sql | 37 +++ ...BrAPIOntologyControllerIntegrationTest.sql | 21 ++ 7 files changed, 438 insertions(+), 44 deletions(-) create mode 100644 src/test/resources/sql/OntologyControllerIntegrationTest.sql create mode 100644 src/test/resources/sql/brapi/BrAPIOntologyControllerIntegrationTest.sql 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 9f9fed262..bf7801f8e 100644 --- a/src/main/java/org/breedinginsight/api/v1/controller/OntologyController.java +++ b/src/main/java/org/breedinginsight/api/v1/controller/OntologyController.java @@ -1,27 +1,43 @@ package org.breedinginsight.api.v1.controller; import io.micronaut.http.HttpResponse; +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.ProgramSecuredRoleGroup; import org.breedinginsight.api.auth.SecurityService; +import org.breedinginsight.api.model.v1.request.SharedOntologyProgramRequest; import org.breedinginsight.api.model.v1.response.DataResponse; import org.breedinginsight.api.model.v1.response.Response; +import org.breedinginsight.api.model.v1.response.metadata.Metadata; +import org.breedinginsight.api.model.v1.response.metadata.Pagination; +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.services.OntologyService; +import org.breedinginsight.services.exceptions.UnprocessableEntityException; +import org.breedinginsight.services.exceptions.ValidatorException; import javax.inject.Inject; -import javax.ws.rs.QueryParam; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; +@Slf4j +@Controller("/${micronaut.bi.api.version}") public class OntologyController { private SecurityService securityService; private OntologyService ontologyService; @Inject - private OntologyController(SecurityService securityService, OntologyService ontologyService) { + public OntologyController(SecurityService securityService, OntologyService ontologyService) { this.securityService = securityService; this.ontologyService = ontologyService; } @@ -46,10 +62,17 @@ private OntologyController(SecurityService securityService, OntologyService onto */ @Get("/programs/{programId}/ontology/shared/programs{?shared}") @Produces(MediaType.APPLICATION_JSON) - @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL}) - public HttpResponse>> getAvailablePrograms( + @AddMetadata + @Secured(SecurityRule.IS_AUTHENTICATED) + public HttpResponse>> getAvailablePrograms( @PathVariable UUID programId, @QueryValue(defaultValue = "false") Boolean shared) { - return HttpResponse.ok(); + List sharedPrograms = ontologyService.getSharedOntology(programId, shared); + List metadataStatus = new ArrayList<>(); + metadataStatus.add(new Status(StatusCode.INFO, "Successful Creation")); + 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); } /** @@ -70,9 +93,23 @@ public HttpResponse>> getAvailablePrograms( @Post("/programs/{programId}/ontology/shared/programs") @Produces(MediaType.APPLICATION_JSON) @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL}) - public HttpResponse>> shareOntology( - @PathVariable UUID programId) { - return HttpResponse.ok(); + public HttpResponse>> shareOntology( + @PathVariable UUID programId, @Body List request) { + try { + List sharedPrograms = 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); + Metadata metadata = new Metadata(pagination, metadataStatus); + Response> response = new Response(metadata, new DataResponse<>(sharedPrograms)); + 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) { + return HttpResponse.status(HttpStatus.UNPROCESSABLE_ENTITY, e.getMessage()); + } } /** diff --git a/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java b/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java index 46a7e27bb..db0fbb16f 100644 --- a/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java +++ b/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java @@ -31,12 +31,14 @@ import org.jooq.Result; import javax.inject.Inject; +import javax.inject.Singleton; import java.util.ArrayList; import java.util.List; import java.util.UUID; import static org.breedinginsight.dao.db.Tables.*; +@Singleton public class ProgramOntologyDAO extends ProgramOntologyDao { private DSLContext dsl; @@ -80,4 +82,8 @@ public List getSharedPrograms(UUID programId) { public void createSharedOntologies(List shareRecords) { programSharedOntologyDao.insert(shareRecords); } + + public List getSharedOntologies(UUID programId) { + return programSharedOntologyDao.fetchByProgramId(programId); + } } diff --git a/src/main/java/org/breedinginsight/model/SharedProgram.java b/src/main/java/org/breedinginsight/model/SharedProgram.java index 9c11fb992..24c12f0ea 100644 --- a/src/main/java/org/breedinginsight/model/SharedProgram.java +++ b/src/main/java/org/breedinginsight/model/SharedProgram.java @@ -10,8 +10,9 @@ @AllArgsConstructor @NoArgsConstructor public class SharedProgram { - private UUID program_id; - private String program_name; + private UUID programId; + private String programName; private Boolean shared; + private Boolean accepted; private Boolean editable; } diff --git a/src/main/java/org/breedinginsight/services/OntologyService.java b/src/main/java/org/breedinginsight/services/OntologyService.java index 938ece75b..f10319323 100644 --- a/src/main/java/org/breedinginsight/services/OntologyService.java +++ b/src/main/java/org/breedinginsight/services/OntologyService.java @@ -9,11 +9,13 @@ import org.breedinginsight.dao.db.tables.pojos.ProgramSharedOntologyEntity; import org.breedinginsight.daos.ProgramDAO; import org.breedinginsight.daos.ProgramOntologyDAO; +import org.breedinginsight.daos.TraitDAO; import org.breedinginsight.model.Program; import org.breedinginsight.model.SharedProgram; import org.breedinginsight.services.exceptions.UnprocessableEntityException; import org.breedinginsight.services.exceptions.ValidatorException; +import javax.inject.Inject; import javax.inject.Singleton; import javax.validation.constraints.NotNull; import java.util.*; @@ -24,10 +26,13 @@ public class OntologyService { private ProgramDAO programDAO; private ProgramOntologyDAO programOntologyDAO; + private TraitDAO traitDAO; - public OntologyService(ProgramDAO programDAO, ProgramOntologyDAO programOntologyDAO) { + @Inject + public OntologyService(ProgramDAO programDAO, ProgramOntologyDAO programOntologyDAO, TraitDAO traitDAO) { this.programDAO = programDAO; this.programOntologyDAO = programOntologyDAO; + this.traitDAO = traitDAO; } /** @@ -41,30 +46,37 @@ public List getSharedOntology(@NotNull UUID programId, @NotNull Boolean sharedOn // Get program with that id Program program = getProgram(programId); - // Get programs of same species and brapi server from db - // TODO: Test if localhost vs localhost/brapi/v2 makes a difference - List matchingPrograms = getMatchingPrograms(program); + List formattedPrograms = getSharedProgramsFormatted(program); + Set sharedProgramIds = formattedPrograms.stream().map(SharedProgram::getProgramId).collect(Collectors.toSet()); + + // Add other programs + if (!sharedOnly) { + // TODO: Test if localhost vs localhost/brapi/v2 makes a difference + List matchingPrograms = getMatchingPrograms(program); + formattedPrograms.addAll(matchingPrograms.stream() + .filter(matchingProgram -> !sharedProgramIds.contains(matchingProgram.getId())) + .map(matchingProgram -> formatResponse(matchingProgram)) + .collect(Collectors.toList())); + } - // Get shared ontology programs for db - List sharedPrograms = programOntologyDAO.getSharedPrograms(program.getId()); + return formattedPrograms; + } - // Format response - // Store shared programs in map for lookup + private List getSharedProgramsFormatted(Program program) { + // Get shared ontology records + List sharedOntologies = programOntologyDAO.getSharedOntologies(program.getId()); + // Get the programs ontology is shared with + List sharedPrograms = programDAO.get( + sharedOntologies.stream().map(ProgramSharedOntologyEntity::getSharedProgramId).collect(Collectors.toList())); + // Get the programs in a lookup map Map sharedProgramsMap = new HashMap<>(); sharedPrograms.stream().forEach(sharedProgram -> sharedProgramsMap.put(sharedProgram.getId(), sharedProgram)); - // Loop through or matching programs, formatting and labeling shared ones. - // TODO: Need to check if these programs have observations - List formattedPrograms = new ArrayList<>(); - for (Program matchingProgram: matchingPrograms) { - SharedProgram formattedProgram = formatResponse(matchingProgram, - sharedProgramsMap.containsKey(matchingProgram.getId()), false); - - formattedPrograms.add(formattedProgram); - } - - - return formattedPrograms; + // Format shared programs response + return sharedOntologies.stream().map(sharedOntology -> + formatResponse(sharedProgramsMap.get(sharedOntology.getSharedProgramId()), sharedOntology, + ontologyIsEditable(sharedOntology))) + .collect(Collectors.toList()); } private List getMatchingPrograms(Program program) { @@ -89,14 +101,39 @@ private Program getProgram(UUID programId) { return programs.get(0); } - private SharedProgram formatResponse(Program program, Boolean shared, Boolean editable) { + private SharedProgram formatResponse(Program program, ProgramSharedOntologyEntity programSharedOntologyEntity, Boolean editable) { + return SharedProgram.builder() + .programId(program.getId()) + .programName(program.getName()) + .shared(true) + .editable(editable) + .accepted(programSharedOntologyEntity.getActive()) + .build(); + } + + private SharedProgram formatResponse(Program program) { return SharedProgram.builder() - .program_id(program.getId()) - .program_name(program.getName()) - .shared(shared) + .programId(program.getId()) + .programName(program.getName()) + .shared(false) .build(); } + private Boolean ontologyIsEditable(ProgramSharedOntologyEntity sharedOntologyEntity) { + + if (sharedOntologyEntity.getActive()) { + // Get all trait ids for the program + List traitIds = traitDAO.getTraitsByProgramId(sharedOntologyEntity.getSharedProgramId()).stream() + .map(trait -> trait.getId()) + .collect(Collectors.toList()); + + // Get all observations for the ontology + return traitDAO.getObservationsForTraits(traitIds).isEmpty(); + } else { + return true; + } + } + /** * Processes share requests for a list of programs. Will return a ValidationError if there are issues @@ -106,10 +143,18 @@ private SharedProgram formatResponse(Program program, Boolean shared, Boolean ed * @param programRequests -- List of programs to share ontology with * @return Lst */ - public List shareOntology(@NotNull UUID programId, AuthenticatedUser actingUser, List programRequests) throws ValidatorException { + public List shareOntology(@NotNull UUID programId, AuthenticatedUser actingUser, List programRequests) throws ValidatorException, UnprocessableEntityException { + // Get program with that id Program program = getProgram(programId); + // Don't allow to share with self + for (SharedOntologyProgramRequest request: programRequests) { + if (request.getId().equals(program.getId())) { + throw new UnprocessableEntityException("Program cannot share ontology with itself"); + } + } + // Check shareability, same brapi server, same species List matchingPrograms = getMatchingPrograms(program); Set matchingProgramsSet = new HashSet<>(); @@ -147,15 +192,9 @@ public List shareOntology(@NotNull UUID programId, AuthenticatedUser actingUser, programOntologyDAO.createSharedOntologies(shareRecords); // Query return data - List allSharedPrograms = programOntologyDAO.getSharedPrograms(programId); - List newSharedPrograms = allSharedPrograms.stream() - .filter(sharedProgram -> shareProgramIdsSet.contains(sharedProgram.getId())) + return getSharedProgramsFormatted(program).stream() + .filter(sharedProgram -> shareProgramIdsSet.contains(sharedProgram.getProgramId())) .collect(Collectors.toList()); - List sharedPrograms = newSharedPrograms.stream().map( - newSharedProgram -> formatResponse(newSharedProgram, false, true) - ).collect(Collectors.toList()); - - return sharedPrograms; } /** 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 bbbf82399..2b7b127c4 100644 --- a/src/test/java/org/breedinginsight/api/v1/controller/OntologyControllerIntegrationTest.java +++ b/src/test/java/org/breedinginsight/api/v1/controller/OntologyControllerIntegrationTest.java @@ -1,49 +1,281 @@ package org.breedinginsight.api.v1.controller; +import com.google.gson.*; import io.kowalski.fannypack.FannyPack; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; import io.micronaut.http.client.RxHttpClient; import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +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; +import org.breedinginsight.api.model.v1.request.SpeciesRequest; +import org.breedinginsight.dao.db.enums.DataType; +import org.breedinginsight.dao.db.tables.pojos.SpeciesEntity; +import org.breedinginsight.daos.ProgramDAO; +import org.breedinginsight.daos.SpeciesDAO; +import org.breedinginsight.daos.UserDAO; +import org.breedinginsight.model.*; +import org.jooq.DSLContext; import org.junit.jupiter.api.*; import javax.inject.Inject; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static io.micronaut.http.HttpRequest.GET; +import static io.micronaut.http.HttpRequest.POST; +import static org.junit.jupiter.api.Assertions.*; + @MicronautTest @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class OntologyControllerIntegrationTest extends BrAPITest { private FannyPack fp; + private FannyPack securityFp; + private FannyPack brapiFp; + private FannyPack brapiObservationFp; + private Program mainProgram; + private Program otherProgram; + + @Inject + private DSLContext dsl; + @Inject + private ProgramDAO programDAO; + @Inject + private UserDAO userDAO; + @Inject + private SpeciesDAO speciesDAO; @Inject @Client("/${micronaut.bi.api.version}") private RxHttpClient client; + private Gson gson = new GsonBuilder().registerTypeAdapter(OffsetDateTime.class, (JsonDeserializer) + (json, type, context) -> OffsetDateTime.parse(json.getAsString())) + .create(); + @AfterAll public void finish() { super.stopContainers(); } @BeforeAll void setup() throws Exception { // Create two programs with fanny pack + fp = FannyPack.fill("src/test/resources/sql/OntologyControllerIntegrationTest.sql"); + securityFp = FannyPack.fill("src/test/resources/sql/ProgramSecuredAnnotationRuleIntegrationTest.sql"); + brapiFp = FannyPack.fill("src/test/resources/sql/brapi/species.sql"); + brapiObservationFp = FannyPack.fill("src/test/resources/sql/brapi/BrAPIOntologyControllerIntegrationTest.sql"); + + User testUser = userDAO.getUserByOrcId(TestTokenValidator.TEST_USER_ORCID).get(); + super.getBrapiDsl().execute(brapiFp.get("InsertSpecies")); + dsl.execute(securityFp.get("InsertSystemRoleAdmin"), testUser.getId().toString()); + + SpeciesEntity validSpecies = speciesDAO.findAll().get(0); + SpeciesRequest speciesRequest = SpeciesRequest.builder() + .commonName(validSpecies.getCommonName()) + .id(validSpecies.getId()) + .build(); + ProgramRequest programRequest1 = ProgramRequest.builder() + .name("Test Program1") + .abbreviation("test1") + .documentationUrl("localhost:8080") + .objective("To test things") + .species(speciesRequest) + .key("OTA") + .build(); + ProgramRequest programRequest2 = ProgramRequest.builder() + .name("Test Program2") + .abbreviation("test2") + .documentationUrl("localhost:8080") + .objective("To test things") + .species(speciesRequest) + .key("OTB") + .build(); + ProgramRequest programRequest3 = ProgramRequest.builder() + .name("Test Program3") + .abbreviation("test3") + .documentationUrl("localhost:8080") + .objective("To test things") + .species(speciesRequest) + .key("OTC") + .build(); + + TestUtils.insertAndFetchTestProgram(gson, client, programRequest1); + TestUtils.insertAndFetchTestProgram(gson, client, programRequest2); + TestUtils.insertAndFetchTestProgram(gson, client, programRequest3); + + // Get main program + List programs = programDAO.getAll(); + mainProgram = programs.get(0); + otherProgram = programs.get(1); + + dsl.execute(securityFp.get("InsertProgramRolesBreeder"), testUser.getId().toString(), mainProgram.getId().toString()); + dsl.execute(securityFp.get("InsertProgramRolesBreeder"), testUser.getId().toString(), otherProgram.getId().toString()); + + // Add trait to program + addTrait(otherProgram); + // Add a single observation to all traits + super.getBrapiDsl().execute(brapiObservationFp.get("AddObservations")); + } + + private void addTrait(Program program) { + + // Add a trait + Trait trait = new Trait(); + trait.setTraitDescription("trait 1 description"); + trait.setEntity("entity1"); + trait.setObservationVariableName("Test Trait"); + trait.setProgramObservationLevel(ProgramObservationLevel.builder().name("Plant").build()); + Scale scale1 = new Scale(); + scale1.setScaleName("Test Scale"); + scale1.setDataType(DataType.TEXT); + Method method1 = new Method(); + trait.setScale(scale1); + trait.setMethod(method1); + trait.setTraitClass("Pheno trait"); + trait.setAttribute("leaf length"); + trait.setMainAbbreviation("abbrev1"); + trait.setSynonyms(List.of("test1", "test2")); + trait.getMethod().setMethodClass("Estimation"); + trait.getMethod().setDescription("A method"); + + 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()); } @Test @Order(1) - void getAllProgramsNoPrograms() { + void getAllProgramsNoSharedPrograms() { + String url = String.format("/programs/%s/ontology/shared/programs", mainProgram.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(3, data.size(), "Wrong number of programs returned"); + // Check all are not shared and are inactive + for (JsonElement element: data) { + JsonObject program = element.getAsJsonObject(); + + 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"); + } } @Test @Order(2) 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())); + 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 + ); + + 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("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 true"); + } } @Test @Order(3) void getAllProgramsSharedPrograms() { + String url = String.format("/programs/%s/ontology/shared/programs", mainProgram.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(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())) { + 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"); + } + + } + } + + @Test + @Order(3) + void getOnlySharedPrograms() { + String url = String.format("/programs/%s/ontology/shared/programs?shared=true", mainProgram.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("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 @@ -51,4 +283,25 @@ void getAllProgramsSharedPrograms() { void revokeProgram() { } + + @Test + void shareSelfError() { + // Test that program cannot share with themselves + + String url = String.format("/programs/%s/ontology/shared/programs", mainProgram.getId()); + List requests = new ArrayList<>(); + requests.add(new SharedOntologyProgramRequest(mainProgram.getId(), mainProgram.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()); + } } diff --git a/src/test/resources/sql/OntologyControllerIntegrationTest.sql b/src/test/resources/sql/OntologyControllerIntegrationTest.sql new file mode 100644 index 000000000..74dca6e9d --- /dev/null +++ b/src/test/resources/sql/OntologyControllerIntegrationTest.sql @@ -0,0 +1,37 @@ +-- name: CopyrightNotice +/* + * 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. + */ + +-- name: InsertPrograms +insert into program (species_id, name, abbreviation, documentation_url, objective, created_by, updated_by, key) +select species.id, 'Test Program 1', 'test1', 'localhost:8080', 'To test things', bi_user.id, bi_user.id, 'OTA' from species + join bi_user on bi_user.name = 'Test User' limit 1; +insert into program (species_id, name, abbreviation, documentation_url, objective, created_by, updated_by, key) +select species.id, 'Test Program 2', 'test2', 'localhost:8080', 'To test things', bi_user.id, bi_user.id, 'OTB' from species + join bi_user on bi_user.name = 'Test User' limit 1; +insert into program (species_id, name, abbreviation, documentation_url, objective, created_by, updated_by, key) +select species.id, 'Test Program 3', 'test3', 'localhost:8080', 'To test things', bi_user.id, bi_user.id, 'OTC' from species + join bi_user on bi_user.name = 'Test User' limit 1; + +-- name: InsertTestUserProgramAccess +insert into program_user_role (user_id, program_id, role_id, created_by, updated_by) +select + ?::uuid, ?::uuid, role.id, bi_user.id, bi_user.id +from + bi_user + join role on role.domain = 'breeder' +where bi_user.name = 'system'; \ No newline at end of file diff --git a/src/test/resources/sql/brapi/BrAPIOntologyControllerIntegrationTest.sql b/src/test/resources/sql/brapi/BrAPIOntologyControllerIntegrationTest.sql new file mode 100644 index 000000000..df07c0dfa --- /dev/null +++ b/src/test/resources/sql/brapi/BrAPIOntologyControllerIntegrationTest.sql @@ -0,0 +1,21 @@ +-- name: CopyrightNotice +/* + * 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. + */ + +-- 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 From f1e899bba749df3266a1fb3dfba5b5183e3a867a Mon Sep 17 00:00:00 2001 From: Chris T Date: Mon, 21 Mar 2022 15:23:11 -0400 Subject: [PATCH 3/9] delete shared ontology: endpoint and tests --- .../api/v1/controller/OntologyController.java | 20 +++- .../daos/ProgramOntologyDAO.java | 13 +++ .../services/OntologyService.java | 25 ++++- .../OntologyControllerIntegrationTest.java | 106 +++++++++++++++++- 4 files changed, 153 insertions(+), 11 deletions(-) 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 bf7801f8e..1e5eb78c0 100644 --- a/src/main/java/org/breedinginsight/api/v1/controller/OntologyController.java +++ b/src/main/java/org/breedinginsight/api/v1/controller/OntologyController.java @@ -21,6 +21,7 @@ import org.breedinginsight.model.SharedProgram; import org.breedinginsight.model.Trait; import org.breedinginsight.services.OntologyService; +import org.breedinginsight.services.exceptions.DoesNotExistException; import org.breedinginsight.services.exceptions.UnprocessableEntityException; import org.breedinginsight.services.exceptions.ValidatorException; @@ -54,8 +55,9 @@ public OntologyController(SecurityService securityService, OntologyService ontol * { * name: string, -- Program name * id: UUID, -- Program ID - * shared: true, - * status: ACCEPTED | PENDING + * shared: boolean, + * accepted: boolean || null, -- null if shared = false + * editable: boolean || null -- null if shared = false * } * ] * } @@ -84,8 +86,9 @@ public HttpResponse>> getAvailablePrograms( * { * name: string, -- Program name * id: UUID, -- Program ID - * shared: true, - * status: ACCEPTED | PENDING + * shared: boolean, + * accepted: boolean, + * editable: boolean * } * ] * } @@ -125,6 +128,13 @@ public HttpResponse>> shareOntology( @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL}) public HttpResponse>> revokeOntology( @PathVariable UUID programId, @PathVariable UUID sharedProgramId) { - return HttpResponse.ok(); + try { + ontologyService.revokeOntology(programId, sharedProgramId); + return HttpResponse.ok(); + } catch (UnprocessableEntityException e) { + return HttpResponse.status(HttpStatus.UNPROCESSABLE_ENTITY, e.getMessage()); + } catch (DoesNotExistException e) { + return HttpResponse.status(HttpStatus.NOT_FOUND, e.getMessage()); + } } } diff --git a/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java b/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java index db0fbb16f..afd0cd843 100644 --- a/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java +++ b/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java @@ -34,7 +34,9 @@ import javax.inject.Singleton; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.UUID; +import java.util.stream.Collectors; import static org.breedinginsight.dao.db.Tables.*; @@ -86,4 +88,15 @@ public void createSharedOntologies(List shareRecord public List getSharedOntologies(UUID programId) { return programSharedOntologyDao.fetchByProgramId(programId); } + + public Optional getSharedOntologyById(UUID programId, UUID sharedProgramId) { + List sharedOntologies = getSharedOntologies(programId).stream() + .filter(programSharedOntologyEntity -> programSharedOntologyEntity.getSharedProgramId().equals(sharedProgramId)) + .collect(Collectors.toList()); + return sharedOntologies.size() > 0 ? Optional.of(sharedOntologies.get(0)) : Optional.empty(); + } + + public void revokeSharedOntology(ProgramSharedOntologyEntity sharedOntology) { + programSharedOntologyDao.delete(sharedOntology); + } } diff --git a/src/main/java/org/breedinginsight/services/OntologyService.java b/src/main/java/org/breedinginsight/services/OntologyService.java index f10319323..25a81e45e 100644 --- a/src/main/java/org/breedinginsight/services/OntologyService.java +++ b/src/main/java/org/breedinginsight/services/OntologyService.java @@ -12,12 +12,14 @@ import org.breedinginsight.daos.TraitDAO; import org.breedinginsight.model.Program; import org.breedinginsight.model.SharedProgram; +import org.breedinginsight.services.exceptions.DoesNotExistException; import org.breedinginsight.services.exceptions.UnprocessableEntityException; import org.breedinginsight.services.exceptions.ValidatorException; import javax.inject.Inject; import javax.inject.Singleton; import javax.validation.constraints.NotNull; +import javax.ws.rs.NotFoundException; import java.util.*; import java.util.stream.Collectors; @@ -143,7 +145,7 @@ private Boolean ontologyIsEditable(ProgramSharedOntologyEntity sharedOntologyEnt * @param programRequests -- List of programs to share ontology with * @return Lst */ - 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 { // Get program with that id Program program = getProgram(programId); @@ -155,6 +157,9 @@ public List shareOntology(@NotNull UUID programId, AuthenticatedUser actingUser, } } + // Don't allow shared if program is already subscribe to shared ontology + + // Check shareability, same brapi server, same species List matchingPrograms = getMatchingPrograms(program); Set matchingProgramsSet = new HashSet<>(); @@ -202,10 +207,22 @@ public List shareOntology(@NotNull UUID programId, AuthenticatedUser actingUser, * @param programId -- Program that owns the ontology. * @param sharedProgramId -- Program to revoke shared ontology access from */ - public void revokeOntology(@NotNull UUID programId, @NotNull UUID sharedProgramId) { - // TODO: Check that shared program is still unshareable. No observations yet. + public void revokeOntology(@NotNull UUID programId, @NotNull UUID sharedProgramId) throws UnprocessableEntityException, DoesNotExistException { + // Check that program exists - // TODO: Remove record from db + // Check that shared program exists + Optional optionalSharedOntology = programOntologyDAO.getSharedOntologyById(programId, sharedProgramId); + if (optionalSharedOntology.isEmpty()) { + throw new DoesNotExistException("Shared program id was not found"); + } + ProgramSharedOntologyEntity sharedOntology = optionalSharedOntology.get(); + + // Check that shared program is still editable. No observations yet. + if (!ontologyIsEditable(sharedOntology)) { + throw new UnprocessableEntityException("Shared ontology can not be removed from this program."); + } + // Remove record from db + programOntologyDAO.revokeSharedOntology(sharedOntology); } } 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 2b7b127c4..c710c30a3 100644 --- a/src/test/java/org/breedinginsight/api/v1/controller/OntologyControllerIntegrationTest.java +++ b/src/test/java/org/breedinginsight/api/v1/controller/OntologyControllerIntegrationTest.java @@ -34,9 +34,9 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.UUID; -import static io.micronaut.http.HttpRequest.GET; -import static io.micronaut.http.HttpRequest.POST; +import static io.micronaut.http.HttpRequest.*; import static org.junit.jupiter.api.Assertions.*; @MicronautTest @@ -50,6 +50,7 @@ public class OntologyControllerIntegrationTest extends BrAPITest { private FannyPack brapiObservationFp; private Program mainProgram; private Program otherProgram; + private Program thirdProgram; @Inject private DSLContext dsl; @@ -121,6 +122,7 @@ void setup() throws Exception { List programs = programDAO.getAll(); mainProgram = programs.get(0); otherProgram = programs.get(1); + thirdProgram = programs.get(2); dsl.execute(securityFp.get("InsertProgramRolesBreeder"), testUser.getId().toString(), mainProgram.getId().toString()); dsl.execute(securityFp.get("InsertProgramRolesBreeder"), testUser.getId().toString(), otherProgram.getId().toString()); @@ -221,6 +223,13 @@ void addSharedPrograms() { } } + @Test + @Order(2) + void shareOntologySubscribedToOtherProgram() { + // TODO: When subscribe ontology card is done + // Cannot share ontology if you are using a shared ontology + } + @Test @Order(3) void getAllProgramsSharedPrograms() { @@ -282,6 +291,20 @@ void getOnlySharedPrograms() { @Order(4) void revokeProgram() { + 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 + ); + + HttpResponse response = call.blockingFirst(); + assertEquals(HttpStatus.OK, response.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 } @Test @@ -304,4 +327,83 @@ void shareSelfError() { }); assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, e.getStatus()); } + + @Test + void revokeOntologyProgramNotExist() { + String url = String.format("/programs/%s/ontology/shared/programs/%s", UUID.randomUUID(), 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.NOT_FOUND, e.getStatus()); + } + + @Test + void revokeOntologySharedProgramNotExist() { + String url = String.format("/programs/%s/ontology/shared/programs/%s", mainProgram.getId(), UUID.randomUUID()); + 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.NOT_FOUND, e.getStatus()); + } + + @Test + void revokeOntologySharedProgramNotShared() { + 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 + ); + + HttpClientResponseException e = Assertions.assertThrows(HttpClientResponseException.class, () -> { + HttpResponse response = call.blockingFirst(); + }); + assertEquals(HttpStatus.NOT_FOUND, e.getStatus()); + } + + @Test + void shareOntologyProgramNotExist() { + + String url = String.format("/programs/%s/ontology/shared/programs", mainProgram.getId()); + List requests = new ArrayList<>(); + requests.add(new SharedOntologyProgramRequest(UUID.randomUUID(), "I don't exist")); + 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 + void shareOntologySharedProgramNotExist() { + + String url = String.format("/programs/%s/ontology/shared/programs", UUID.randomUUID()); + List requests = new ArrayList<>(); + requests.add(new SharedOntologyProgramRequest(otherProgram.getId(), otherProgram.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.NOT_FOUND, e.getStatus()); + } } From d28fe5334694f95ce045f7aba433175ce185fdcc Mon Sep 17 00:00:00 2001 From: Chris T Date: Mon, 28 Mar 2022 09:34:20 -0400 Subject: [PATCH 4/9] shared ontology: small naming fixes --- .../model/v1/request/SharedOntologyProgramRequest.java | 2 +- .../api/v1/controller/OntologyController.java | 8 ++++---- .../org/breedinginsight/services/OntologyService.java | 10 ++++++---- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/breedinginsight/api/model/v1/request/SharedOntologyProgramRequest.java b/src/main/java/org/breedinginsight/api/model/v1/request/SharedOntologyProgramRequest.java index b23902cbd..1f745ef49 100644 --- a/src/main/java/org/breedinginsight/api/model/v1/request/SharedOntologyProgramRequest.java +++ b/src/main/java/org/breedinginsight/api/model/v1/request/SharedOntologyProgramRequest.java @@ -15,6 +15,6 @@ @Introspected public class SharedOntologyProgramRequest { @NotBlank - private UUID id; + private UUID programId; private String programName; } 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 1e5eb78c0..9c12dfd6d 100644 --- a/src/main/java/org/breedinginsight/api/v1/controller/OntologyController.java +++ b/src/main/java/org/breedinginsight/api/v1/controller/OntologyController.java @@ -53,8 +53,8 @@ public OntologyController(SecurityService securityService, OntologyService ontol * { * data: [ * { - * name: string, -- Program name - * id: UUID, -- Program ID + * programName: string, -- Program name + * programId: UUID, -- Program ID * shared: boolean, * accepted: boolean || null, -- null if shared = false * editable: boolean || null -- null if shared = false @@ -84,8 +84,8 @@ public HttpResponse>> getAvailablePrograms( * { * data: [ * { - * name: string, -- Program name - * id: UUID, -- Program ID + * programName: string, -- Program name + * programId: UUID, -- Program ID * shared: boolean, * accepted: boolean, * editable: boolean diff --git a/src/main/java/org/breedinginsight/services/OntologyService.java b/src/main/java/org/breedinginsight/services/OntologyService.java index 25a81e45e..4eec7e6eb 100644 --- a/src/main/java/org/breedinginsight/services/OntologyService.java +++ b/src/main/java/org/breedinginsight/services/OntologyService.java @@ -87,7 +87,9 @@ private List getMatchingPrograms(Program program) { for (Program candidate: allPrograms) { if (candidate.getSpecies().getId().equals(program.getSpecies().getId()) && - candidate.getBrapiUrl().equals(program.getBrapiUrl())) { + candidate.getBrapiUrl().equals(program.getBrapiUrl()) && + !candidate.getId().equals(program.getId()) + ) { matchingPrograms.add(candidate); } @@ -152,7 +154,7 @@ public List shareOntology(@NotNull UUID programId, AuthenticatedU // Don't allow to share with self for (SharedOntologyProgramRequest request: programRequests) { - if (request.getId().equals(program.getId())) { + if (request.getProgramId().equals(program.getId())) { throw new UnprocessableEntityException("Program cannot share ontology with itself"); } } @@ -169,13 +171,13 @@ public List shareOntology(@NotNull UUID programId, AuthenticatedU ValidationErrors validationErrors = new ValidationErrors(); for (int i = 0; i < programRequests.size(); i++) { SharedOntologyProgramRequest programRequest = programRequests.get(i); - if (!matchingProgramsSet.contains(programRequest.getId())) { + if (!matchingProgramsSet.contains(programRequest.getProgramId())) { ValidationError error = new ValidationError("program", "Program does not have same species or brapi server.", HttpStatus.UNPROCESSABLE_ENTITY); validationErrors.addError(i, error); } else { - shareProgramIdsSet.add(programRequest.getId()); + shareProgramIdsSet.add(programRequest.getProgramId()); } } From 96d3165eb4168736fcdeae4b414dd7b976baeb27 Mon Sep 17 00:00:00 2001 From: Chris T Date: Mon, 28 Mar 2022 14:58:37 -0400 Subject: [PATCH 5/9] share ontology: cleanup --- .../daos/ProgramOntologyDAO.java | 8 ++++++++ .../services/OntologyService.java | 15 +++++++++++---- .../OntologyControllerIntegrationTest.java | 18 +++++++++--------- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java b/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java index afd0cd843..db45c04c8 100644 --- a/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java +++ b/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java @@ -99,4 +99,12 @@ public Optional getSharedOntologyById(UUID programI public void revokeSharedOntology(ProgramSharedOntologyEntity sharedOntology) { programSharedOntologyDao.delete(sharedOntology); } + + public boolean programSubscribedSharedOntology(UUID programId) { + List shareRecords = programSharedOntologyDao.fetchBySharedProgramId(programId); + return !shareRecords.stream() + .filter(shareRecord -> shareRecord.getActive()) + .collect(Collectors.toList()) + .isEmpty(); + } } diff --git a/src/main/java/org/breedinginsight/services/OntologyService.java b/src/main/java/org/breedinginsight/services/OntologyService.java index 4eec7e6eb..32776af21 100644 --- a/src/main/java/org/breedinginsight/services/OntologyService.java +++ b/src/main/java/org/breedinginsight/services/OntologyService.java @@ -29,12 +29,14 @@ public class OntologyService { private ProgramDAO programDAO; private ProgramOntologyDAO programOntologyDAO; private TraitDAO traitDAO; + private TraitService traitService; @Inject - public OntologyService(ProgramDAO programDAO, ProgramOntologyDAO programOntologyDAO, TraitDAO traitDAO) { + public OntologyService(ProgramDAO programDAO, ProgramOntologyDAO programOntologyDAO, TraitDAO traitDAO, TraitService traitService) { this.programDAO = programDAO; this.programOntologyDAO = programOntologyDAO; this.traitDAO = traitDAO; + this.traitService = traitService; } /** @@ -126,13 +128,16 @@ 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 = traitDAO.getTraitsByProgramId(sharedOntologyEntity.getSharedProgramId()).stream() + List traitIds = traitService.getByProgramId(sharedOntologyEntity.getSharedProgramId(), true).stream() .map(trait -> trait.getId()) .collect(Collectors.toList()); // Get all observations for the ontology return traitDAO.getObservationsForTraits(traitIds).isEmpty(); + */ + return true; } else { return true; } @@ -160,7 +165,9 @@ public List shareOntology(@NotNull UUID programId, AuthenticatedU } // Don't allow shared if program is already subscribe to shared ontology - + if (programOntologyDAO.programSubscribedSharedOntology(programId)) { + throw new UnprocessableEntityException("Program is subscribed to a shared ontology and cannot share its own."); + } // Check shareability, same brapi server, same species List matchingPrograms = getMatchingPrograms(program); @@ -173,7 +180,7 @@ public List shareOntology(@NotNull UUID programId, AuthenticatedU SharedOntologyProgramRequest programRequest = programRequests.get(i); if (!matchingProgramsSet.contains(programRequest.getProgramId())) { ValidationError error = new ValidationError("program", - "Program does not have same species or brapi server.", + String.format("Program %s does not have same species or brapi server.", programRequest.getProgramName()), HttpStatus.UNPROCESSABLE_ENTITY); validationErrors.addError(i, error); } else { 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 c710c30a3..0102287bc 100644 --- a/src/test/java/org/breedinginsight/api/v1/controller/OntologyControllerIntegrationTest.java +++ b/src/test/java/org/breedinginsight/api/v1/controller/OntologyControllerIntegrationTest.java @@ -179,7 +179,7 @@ void getAllProgramsNoSharedPrograms() { JsonObject result = JsonParser.parseString(response.body()).getAsJsonObject().getAsJsonObject("result"); JsonArray data = result.getAsJsonArray("data"); - assertEquals(3, 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) { @@ -243,7 +243,7 @@ void getAllProgramsSharedPrograms() { JsonObject result = JsonParser.parseString(response.body()).getAsJsonObject().getAsJsonObject("result"); JsonArray data = result.getAsJsonArray("data"); - assertEquals(3, 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) { @@ -289,6 +289,13 @@ void getOnlySharedPrograms() { @Test @Order(4) + void revokeOntologyUneditable() { + // TODO: When subscribe ontology card is done + // Ontology cannot be revoke if shared program has accepted and has observations + } + + @Test + @Order(5) void revokeProgram() { String url = String.format("/programs/%s/ontology/shared/programs/%s", mainProgram.getId(), otherProgram.getId()); @@ -300,13 +307,6 @@ void revokeProgram() { assertEquals(HttpStatus.OK, response.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 - } - @Test void shareSelfError() { // Test that program cannot share with themselves From 33b5916eb55a2744b440a9c5293665197cce5d53 Mon Sep 17 00:00:00 2001 From: Chris T Date: Mon, 28 Mar 2022 15:15:11 -0400 Subject: [PATCH 6/9] share ontology: fix migration version conflict --- ...ogy_tables.sql => V0.5.34__created_shared_ontology_tables.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V0.5.33__created_shared_ontology_tables.sql => V0.5.34__created_shared_ontology_tables.sql} (100%) diff --git a/src/main/resources/db/migration/V0.5.33__created_shared_ontology_tables.sql b/src/main/resources/db/migration/V0.5.34__created_shared_ontology_tables.sql similarity index 100% rename from src/main/resources/db/migration/V0.5.33__created_shared_ontology_tables.sql rename to src/main/resources/db/migration/V0.5.34__created_shared_ontology_tables.sql From 3c437bb48d3f50865da0990708f9fdbb3408a26b Mon Sep 17 00:00:00 2001 From: Chris T Date: Mon, 28 Mar 2022 16:03:20 -0400 Subject: [PATCH 7/9] shared ontology: remove unused test sql file --- .../sql/OntologyControllerIntegrationTest.sql | 37 ------------------- 1 file changed, 37 deletions(-) delete mode 100644 src/test/resources/sql/OntologyControllerIntegrationTest.sql diff --git a/src/test/resources/sql/OntologyControllerIntegrationTest.sql b/src/test/resources/sql/OntologyControllerIntegrationTest.sql deleted file mode 100644 index 74dca6e9d..000000000 --- a/src/test/resources/sql/OntologyControllerIntegrationTest.sql +++ /dev/null @@ -1,37 +0,0 @@ --- name: CopyrightNotice -/* - * 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. - */ - --- name: InsertPrograms -insert into program (species_id, name, abbreviation, documentation_url, objective, created_by, updated_by, key) -select species.id, 'Test Program 1', 'test1', 'localhost:8080', 'To test things', bi_user.id, bi_user.id, 'OTA' from species - join bi_user on bi_user.name = 'Test User' limit 1; -insert into program (species_id, name, abbreviation, documentation_url, objective, created_by, updated_by, key) -select species.id, 'Test Program 2', 'test2', 'localhost:8080', 'To test things', bi_user.id, bi_user.id, 'OTB' from species - join bi_user on bi_user.name = 'Test User' limit 1; -insert into program (species_id, name, abbreviation, documentation_url, objective, created_by, updated_by, key) -select species.id, 'Test Program 3', 'test3', 'localhost:8080', 'To test things', bi_user.id, bi_user.id, 'OTC' from species - join bi_user on bi_user.name = 'Test User' limit 1; - --- name: InsertTestUserProgramAccess -insert into program_user_role (user_id, program_id, role_id, created_by, updated_by) -select - ?::uuid, ?::uuid, role.id, bi_user.id, bi_user.id -from - bi_user - join role on role.domain = 'breeder' -where bi_user.name = 'system'; \ No newline at end of file From 5cd7705ae03cb7ac0cd6056f25d3555659136049 Mon Sep 17 00:00:00 2001 From: Chris T Date: Mon, 28 Mar 2022 16:25:38 -0400 Subject: [PATCH 8/9] shared ontology: PR cleanup --- .../api/v1/controller/OntologyController.java | 9 ++--- .../daos/ProgramOntologyDAO.java | 38 ------------------- .../services/OntologyService.java | 4 +- .../OntologyControllerIntegrationTest.java | 2 - 4 files changed, 5 insertions(+), 48 deletions(-) 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 9c12dfd6d..39a21c43d 100644 --- a/src/main/java/org/breedinginsight/api/v1/controller/OntologyController.java +++ b/src/main/java/org/breedinginsight/api/v1/controller/OntologyController.java @@ -70,7 +70,7 @@ 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 Creation")); + 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)); @@ -85,10 +85,7 @@ public HttpResponse>> getAvailablePrograms( * data: [ * { * programName: string, -- Program name - * programId: UUID, -- Program ID - * shared: boolean, - * accepted: boolean, - * editable: boolean + * programId: UUID, -- Required. Program ID * } * ] * } @@ -117,7 +114,7 @@ public HttpResponse>> shareOntology( /** * Revokes access to shared ontology from a program. If program is not currently shared with - * will still return a 200. + * will return 404. * * @param programId * @param sharedProgramId diff --git a/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java b/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java index db45c04c8..2068f34cf 100644 --- a/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java +++ b/src/main/java/org/breedinginsight/daos/ProgramOntologyDAO.java @@ -17,29 +17,19 @@ package org.breedinginsight.daos; -import org.breedinginsight.dao.db.tables.BiUserTable; -import org.breedinginsight.dao.db.tables.ProgramTable; import org.breedinginsight.dao.db.tables.daos.ProgramOntologyDao; import org.breedinginsight.dao.db.tables.daos.ProgramSharedOntologyDao; import org.breedinginsight.dao.db.tables.pojos.ProgramSharedOntologyEntity; -import org.breedinginsight.model.BrAPIConstants; -import org.breedinginsight.model.Program; -import org.breedinginsight.model.User; import org.jooq.Configuration; import org.jooq.DSLContext; -import org.jooq.Record; -import org.jooq.Result; import javax.inject.Inject; import javax.inject.Singleton; -import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; -import static org.breedinginsight.dao.db.Tables.*; - @Singleton public class ProgramOntologyDAO extends ProgramOntologyDao { @@ -53,34 +43,6 @@ public ProgramOntologyDAO(Configuration config, DSLContext dsl, ProgramSharedOnt this.programSharedOntologyDao = programSharedOntologyDao; } - /** - * Retrieves all programs that the given program has shared its ontology with. - * @param programId - */ - public List getSharedPrograms(UUID programId) { - - ProgramTable sharedProgram = PROGRAM.as("sharedProgram"); - - Result queryResult = dsl.select() - .from(PROGRAM) - .join(PROGRAM_SHARED_ONTOLOGY) - .on(PROGRAM.ID.eq(PROGRAM_SHARED_ONTOLOGY.PROGRAM_ID)) - .join(sharedProgram) - .on(PROGRAM_SHARED_ONTOLOGY.SHARED_PROGRAM_ID.eq(sharedProgram.ID)) - .where(PROGRAM.ID.eq(programId)) - .fetch(); - - List sharedPrograms = new ArrayList<>(); - for (Record record: queryResult){ - if (record.getValue(PROGRAM.BRAPI_URL) == null) { - record.setValue(PROGRAM.BRAPI_URL, BrAPIConstants.SYSTEM_DEFAULT.getValue()); - } - Program program = Program.parseSQLRecord(record); - sharedPrograms.add(program); - } - return sharedPrograms; - } - public void createSharedOntologies(List shareRecords) { programSharedOntologyDao.insert(shareRecords); } diff --git a/src/main/java/org/breedinginsight/services/OntologyService.java b/src/main/java/org/breedinginsight/services/OntologyService.java index 32776af21..8a515e0cb 100644 --- a/src/main/java/org/breedinginsight/services/OntologyService.java +++ b/src/main/java/org/breedinginsight/services/OntologyService.java @@ -19,7 +19,6 @@ import javax.inject.Inject; import javax.inject.Singleton; import javax.validation.constraints.NotNull; -import javax.ws.rs.NotFoundException; import java.util.*; import java.util.stream.Collectors; @@ -150,7 +149,7 @@ private Boolean ontologyIsEditable(ProgramSharedOntologyEntity sharedOntologyEnt * * @param programId -- Program that owns the ontology * @param programRequests -- List of programs to share ontology with - * @return Lst + * @return List */ public List shareOntology(@NotNull UUID programId, AuthenticatedUser actingUser, List programRequests) throws ValidatorException, UnprocessableEntityException { @@ -218,6 +217,7 @@ public List shareOntology(@NotNull UUID programId, AuthenticatedU */ public void revokeOntology(@NotNull UUID programId, @NotNull UUID sharedProgramId) throws UnprocessableEntityException, DoesNotExistException { // Check that program exists + Program program = getProgram(programId); // Check that shared program exists Optional optionalSharedOntology = programOntologyDAO.getSharedOntologyById(programId, sharedProgramId); 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 0102287bc..7228822db 100644 --- a/src/test/java/org/breedinginsight/api/v1/controller/OntologyControllerIntegrationTest.java +++ b/src/test/java/org/breedinginsight/api/v1/controller/OntologyControllerIntegrationTest.java @@ -44,7 +44,6 @@ @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class OntologyControllerIntegrationTest extends BrAPITest { - private FannyPack fp; private FannyPack securityFp; private FannyPack brapiFp; private FannyPack brapiObservationFp; @@ -75,7 +74,6 @@ public class OntologyControllerIntegrationTest extends BrAPITest { @BeforeAll void setup() throws Exception { // Create two programs with fanny pack - fp = FannyPack.fill("src/test/resources/sql/OntologyControllerIntegrationTest.sql"); securityFp = FannyPack.fill("src/test/resources/sql/ProgramSecuredAnnotationRuleIntegrationTest.sql"); brapiFp = FannyPack.fill("src/test/resources/sql/brapi/species.sql"); brapiObservationFp = FannyPack.fill("src/test/resources/sql/brapi/BrAPIOntologyControllerIntegrationTest.sql"); From 0e177b199c544514b18ef87240d7883712ceef2a Mon Sep 17 00:00:00 2001 From: Chris T Date: Wed, 30 Mar 2022 14:49:20 -0400 Subject: [PATCH 9/9] shared ontology: restrict endpoints to just breeders --- .../api/v1/controller/OntologyController.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 39a21c43d..cd02ef6ea 100644 --- a/src/main/java/org/breedinginsight/api/v1/controller/OntologyController.java +++ b/src/main/java/org/breedinginsight/api/v1/controller/OntologyController.java @@ -8,6 +8,7 @@ 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; @@ -65,7 +66,7 @@ public OntologyController(SecurityService securityService, OntologyService ontol @Get("/programs/{programId}/ontology/shared/programs{?shared}") @Produces(MediaType.APPLICATION_JSON) @AddMetadata - @Secured(SecurityRule.IS_AUTHENTICATED) + @ProgramSecured(roles = {ProgramSecuredRole.BREEDER}) public HttpResponse>> getAvailablePrograms( @PathVariable UUID programId, @QueryValue(defaultValue = "false") Boolean shared) { List sharedPrograms = ontologyService.getSharedOntology(programId, shared); @@ -92,7 +93,7 @@ public HttpResponse>> getAvailablePrograms( */ @Post("/programs/{programId}/ontology/shared/programs") @Produces(MediaType.APPLICATION_JSON) - @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL}) + @ProgramSecured(roles = {ProgramSecuredRole.BREEDER}) public HttpResponse>> shareOntology( @PathVariable UUID programId, @Body List request) { try { @@ -122,7 +123,7 @@ public HttpResponse>> shareOntology( */ @Delete("/programs/{programId}/ontology/shared/programs/{sharedProgramId}") @Produces(MediaType.APPLICATION_JSON) - @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL}) + @ProgramSecured(roles = {ProgramSecuredRole.BREEDER}) public HttpResponse>> revokeOntology( @PathVariable UUID programId, @PathVariable UUID sharedProgramId) { try {