From 5946e2f024a31bd961e075ddbbf242a8f07adc38 Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Tue, 30 Jul 2024 13:10:43 +0200 Subject: [PATCH 01/24] wip(info-req): add mechanism for submission Signed-off-by: RadovanTomik --- .../v3/InformationRequirementController.java | 7 +- .../v3/InformationSubmissionController.java | 59 ++++++++++++++ .../dto/InformationSubmissionDTO.java | 19 +++++ .../dto/SubmittedInformationDTO.java | 20 +++++ .../InformationRequirementControllerTest.java | 76 +++++++++++++++++++ 5 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationSubmissionController.java create mode 100644 src/main/java/eu/bbmri_eric/negotiator/dto/InformationSubmissionDTO.java create mode 100644 src/main/java/eu/bbmri_eric/negotiator/dto/SubmittedInformationDTO.java diff --git a/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationRequirementController.java b/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationRequirementController.java index 88356c2cb..76bdbaea4 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationRequirementController.java +++ b/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationRequirementController.java @@ -10,6 +10,7 @@ import jakarta.validation.Valid; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.MediaTypes; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -22,7 +23,9 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping(InformationRequirementController.BASE_URL) +@RequestMapping( + value = InformationRequirementController.BASE_URL, + produces = MediaTypes.HAL_JSON_VALUE) @Tag( name = "Information requirements", description = "Set requirements for Resource states in Negotiations.") @@ -72,4 +75,6 @@ public EntityModel updateRequirement( public void deleteRequirement(@PathVariable Long id) { service.deleteInformationRequirement(id); } + + } diff --git a/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationSubmissionController.java b/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationSubmissionController.java new file mode 100644 index 000000000..fb9fc05c2 --- /dev/null +++ b/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationSubmissionController.java @@ -0,0 +1,59 @@ +package eu.bbmri_eric.negotiator.api.controller.v3; + +import eu.bbmri_eric.negotiator.dto.InformationRequirementDTO; +import eu.bbmri_eric.negotiator.dto.InformationSubmissionDTO; +import eu.bbmri_eric.negotiator.dto.SubmittedInformationDTO; +import eu.bbmri_eric.negotiator.dto.negotiation.NegotiationDTO; +import eu.bbmri_eric.negotiator.service.InformationRequirementService; +import eu.bbmri_eric.negotiator.service.NegotiationService; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.MediaTypes; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping( + value = InformationSubmissionController.BASE_URL, + produces = MediaTypes.HAL_JSON_VALUE) +@Tag( + name = "Submit required information", + description = "Submit required information on behalf of a resource in a Negotiation.") +@SecurityRequirement(name = "security_auth") +public class InformationSubmissionController { + private final InformationRequirementService requirementService; + private final NegotiationService negotiationService; + public static final String BASE_URL = "/v3"; + + public InformationSubmissionController( + InformationRequirementService requirementService, NegotiationService negotiationService) { + this.requirementService = requirementService; + this.negotiationService = negotiationService; + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/negotiations/{negotiationId}/info-requirements/{requirementId}") + public String getSummaryInformation( + @PathVariable String negotiationId, @PathVariable Long requirementId) { + NegotiationDTO negotiationDTO = negotiationService.findById(negotiationId, false); + InformationRequirementDTO requirementDTO = + requirementService.getInformationRequirement(requirementId); + return "{}"; + } + + @ResponseStatus(HttpStatus.OK) + @PostMapping("/negotiations/{negotiationId}/info-requirements/{requirementId}") + public EntityModel submitInformation( + @PathVariable String negotiationId, + @PathVariable Long requirementId, + @RequestBody InformationSubmissionDTO dto) { + return EntityModel.of(new SubmittedInformationDTO(1L, dto.getResourceId(), dto.getPayload())); + } +} diff --git a/src/main/java/eu/bbmri_eric/negotiator/dto/InformationSubmissionDTO.java b/src/main/java/eu/bbmri_eric/negotiator/dto/InformationSubmissionDTO.java new file mode 100644 index 000000000..fb13ece15 --- /dev/null +++ b/src/main/java/eu/bbmri_eric/negotiator/dto/InformationSubmissionDTO.java @@ -0,0 +1,19 @@ +package eu.bbmri_eric.negotiator.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(value = JsonInclude.Include.NON_NULL) +public class InformationSubmissionDTO { + @NotNull private Long resourceId; + @NotNull private JsonNode payload; +} diff --git a/src/main/java/eu/bbmri_eric/negotiator/dto/SubmittedInformationDTO.java b/src/main/java/eu/bbmri_eric/negotiator/dto/SubmittedInformationDTO.java new file mode 100644 index 000000000..7631fa2b2 --- /dev/null +++ b/src/main/java/eu/bbmri_eric/negotiator/dto/SubmittedInformationDTO.java @@ -0,0 +1,20 @@ +package eu.bbmri_eric.negotiator.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(value = JsonInclude.Include.NON_NULL) +public class SubmittedInformationDTO { + @NotNull private Long id; + @NotNull private Long resourceId; + @NotNull private JsonNode payload; +} diff --git a/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java b/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java index e7ae7cca2..a661231c9 100644 --- a/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java +++ b/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java @@ -1,16 +1,25 @@ package eu.bbmri_eric.negotiator.integration.api.v3; import static org.hamcrest.core.Is.is; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import eu.bbmri_eric.negotiator.NegotiatorApplication; import eu.bbmri_eric.negotiator.configuration.state_machine.resource.NegotiationResourceEvent; +import eu.bbmri_eric.negotiator.database.model.Negotiation; +import eu.bbmri_eric.negotiator.database.repository.InformationRequirementRepository; +import eu.bbmri_eric.negotiator.database.repository.NegotiationRepository; import eu.bbmri_eric.negotiator.dto.InformationRequirementCreateDTO; +import eu.bbmri_eric.negotiator.dto.InformationRequirementDTO; +import eu.bbmri_eric.negotiator.dto.InformationSubmissionDTO; +import eu.bbmri_eric.negotiator.service.InformationRequirementServiceImpl; import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; @@ -27,7 +36,11 @@ @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) public class InformationRequirementControllerTest { private final String INFO_REQUIREMENT_ENDPOINT = "/v3/info-requirements"; + private final String INFO_SUBMISSION_ENDPOINT = "/v3/negotiations/%s/info-requirements/%s"; private MockMvc mockMvc; + @Autowired private NegotiationRepository negotiationRepository; + @Autowired private InformationRequirementRepository informationRequirementRepository; + @Autowired private InformationRequirementServiceImpl informationRequirementServiceImpl; @BeforeEach void setup(WebApplicationContext wac) { @@ -216,4 +229,67 @@ void findRequirementById_nonExistingId_notFound() throws Exception { .perform(MockMvcRequestBuilders.get(INFO_REQUIREMENT_ENDPOINT + "/" + nonExistingId)) .andExpect(status().isNotFound()); } + + @Test + void getSummary_nonExistingId_notFound() throws Exception { + mockMvc + .perform(MockMvcRequestBuilders.get(INFO_SUBMISSION_ENDPOINT.formatted("1", "1"))) + .andExpect(status().isNotFound()); + } + + @Test + void getSummary_nonExistingRequirement_notFound() throws Exception { + Negotiation negotiation = negotiationRepository.findAll().iterator().next(); + mockMvc + .perform( + MockMvcRequestBuilders.get( + INFO_SUBMISSION_ENDPOINT.formatted(negotiation.getId(), "1"))) + .andExpect(status().isNotFound()); + } + + @Test + void getSummary_noSubmissions_emptyResponse() throws Exception { + Negotiation negotiation = negotiationRepository.findAll().iterator().next(); + InformationRequirementDTO informationRequirementDTO = + informationRequirementServiceImpl.createInformationRequirement( + new InformationRequirementCreateDTO(1L, NegotiationResourceEvent.CONTACT)); + mockMvc + .perform( + MockMvcRequestBuilders.get( + INFO_SUBMISSION_ENDPOINT.formatted( + negotiation.getId(), informationRequirementDTO.getId()))) + .andExpect(status().isOk()) + .andExpect(content().json("{}")); + } + + @Test + void submitInformation_correctPayload_ok() throws Exception { + Negotiation negotiation = negotiationRepository.findAll().iterator().next(); + InformationRequirementDTO informationRequirementDTO = + informationRequirementServiceImpl.createInformationRequirement( + new InformationRequirementCreateDTO(1L, NegotiationResourceEvent.CONTACT)); + String payload = + """ + { + "sample-type": "DNA", + "num-of-subjects": 10, + "num-of-samples": 20, + "volume-per-sample": 5 + } + """; + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonPayload = mapper.readTree(payload); + InformationSubmissionDTO submissionDTO = new InformationSubmissionDTO(1L, jsonPayload); + mockMvc + .perform( + MockMvcRequestBuilders.post( + INFO_SUBMISSION_ENDPOINT.formatted( + negotiation.getId(), informationRequirementDTO.getId())) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(submissionDTO))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").isNumber()) + .andExpect(jsonPath("$.resourceId").value(submissionDTO.getResourceId())) + .andExpect(jsonPath("$.payload.sample-type").value("DNA")); + } } From 7f2888b25251264d235988fb774ad1e2caeae93c Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Tue, 30 Jul 2024 14:08:23 +0200 Subject: [PATCH 02/24] wip(info-req): add mechanism for information submission Signed-off-by: RadovanTomik --- .../v3/InformationSubmissionController.java | 15 +++- .../database/model/InformationSubmission.java | 63 +++++++++++++++ .../InformationSubmissionRepository.java | 7 ++ .../mappers/InformationSubmissionMapper.java | 48 ++++++++++++ .../service/InformationSubmissionService.java | 13 ++++ .../InformationSubmissionServiceImpl.java | 77 +++++++++++++++++++ .../postgresql/V12.0__add_info_submission.sql | 14 ++++ .../InformationRequirementControllerTest.java | 10 ++- 8 files changed, 244 insertions(+), 3 deletions(-) create mode 100644 src/main/java/eu/bbmri_eric/negotiator/database/model/InformationSubmission.java create mode 100644 src/main/java/eu/bbmri_eric/negotiator/database/repository/InformationSubmissionRepository.java create mode 100644 src/main/java/eu/bbmri_eric/negotiator/mappers/InformationSubmissionMapper.java create mode 100644 src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionService.java create mode 100644 src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java create mode 100644 src/main/resources/db/migration/postgresql/V12.0__add_info_submission.sql diff --git a/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationSubmissionController.java b/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationSubmissionController.java index fb9fc05c2..85d469f7c 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationSubmissionController.java +++ b/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationSubmissionController.java @@ -5,6 +5,7 @@ import eu.bbmri_eric.negotiator.dto.SubmittedInformationDTO; import eu.bbmri_eric.negotiator.dto.negotiation.NegotiationDTO; import eu.bbmri_eric.negotiator.service.InformationRequirementService; +import eu.bbmri_eric.negotiator.service.InformationSubmissionService; import eu.bbmri_eric.negotiator.service.NegotiationService; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -30,12 +31,16 @@ public class InformationSubmissionController { private final InformationRequirementService requirementService; private final NegotiationService negotiationService; + private final InformationSubmissionService submissionService; public static final String BASE_URL = "/v3"; public InformationSubmissionController( - InformationRequirementService requirementService, NegotiationService negotiationService) { + InformationRequirementService requirementService, + NegotiationService negotiationService, + InformationSubmissionService submissionService) { this.requirementService = requirementService; this.negotiationService = negotiationService; + this.submissionService = submissionService; } @ResponseStatus(HttpStatus.OK) @@ -54,6 +59,12 @@ public EntityModel submitInformation( @PathVariable String negotiationId, @PathVariable Long requirementId, @RequestBody InformationSubmissionDTO dto) { - return EntityModel.of(new SubmittedInformationDTO(1L, dto.getResourceId(), dto.getPayload())); + return EntityModel.of(submissionService.submit(dto, requirementId, negotiationId)); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/info-submissions/{id}") + public EntityModel getInfoSubmission(@PathVariable Long id) { + return EntityModel.of(submissionService.findById(id)); } } diff --git a/src/main/java/eu/bbmri_eric/negotiator/database/model/InformationSubmission.java b/src/main/java/eu/bbmri_eric/negotiator/database/model/InformationSubmission.java new file mode 100644 index 000000000..92e6196b2 --- /dev/null +++ b/src/main/java/eu/bbmri_eric/negotiator/database/model/InformationSubmission.java @@ -0,0 +1,63 @@ +package eu.bbmri_eric.negotiator.database.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +@Setter +@Getter +@Entity +public class InformationSubmission { + + protected InformationSubmission() {} + + protected InformationSubmission( + Long id, + InformationRequirement requirement, + Resource resource, + Negotiation negotiation, + String payload) { + this.id = id; + this.requirement = requirement; + this.resource = resource; + this.negotiation = negotiation; + this.payload = payload; + } + + public InformationSubmission( + InformationRequirement requirement, + Resource resource, + Negotiation negotiation, + String payload) { + this.requirement = requirement; + this.resource = resource; + this.negotiation = negotiation; + this.payload = payload; + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "requirement_id") + private InformationRequirement requirement; + + @ManyToOne + @JoinColumn(name = "resource_id") + private Resource resource; + + @ManyToOne + @JoinColumn(name = "negotiation_id") + private Negotiation negotiation; + + @JdbcTypeCode(SqlTypes.JSON) + private String payload; +} diff --git a/src/main/java/eu/bbmri_eric/negotiator/database/repository/InformationSubmissionRepository.java b/src/main/java/eu/bbmri_eric/negotiator/database/repository/InformationSubmissionRepository.java new file mode 100644 index 000000000..fd42f5b21 --- /dev/null +++ b/src/main/java/eu/bbmri_eric/negotiator/database/repository/InformationSubmissionRepository.java @@ -0,0 +1,7 @@ +package eu.bbmri_eric.negotiator.database.repository; + +import eu.bbmri_eric.negotiator.database.model.InformationSubmission; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface InformationSubmissionRepository + extends JpaRepository {} diff --git a/src/main/java/eu/bbmri_eric/negotiator/mappers/InformationSubmissionMapper.java b/src/main/java/eu/bbmri_eric/negotiator/mappers/InformationSubmissionMapper.java new file mode 100644 index 000000000..516b02d2b --- /dev/null +++ b/src/main/java/eu/bbmri_eric/negotiator/mappers/InformationSubmissionMapper.java @@ -0,0 +1,48 @@ +package eu.bbmri_eric.negotiator.mappers; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.bbmri_eric.negotiator.database.model.InformationSubmission; +import eu.bbmri_eric.negotiator.dto.SubmittedInformationDTO; +import jakarta.annotation.PostConstruct; +import org.modelmapper.Converter; +import org.modelmapper.ModelMapper; +import org.modelmapper.TypeMap; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class InformationSubmissionMapper { + ModelMapper modelMapper; + + public InformationSubmissionMapper(ModelMapper modelMapper) { + this.modelMapper = modelMapper; + } + + @PostConstruct + public void addMappings() { + TypeMap typeMap = + modelMapper.createTypeMap(InformationSubmission.class, SubmittedInformationDTO.class); + Converter payloadConverter = + p -> { + try { + return payloadConverter(p.getSource()); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); // TODO: raise the correct exception + } + }; + typeMap.addMappings( + mapper -> + mapper + .using(payloadConverter) + .map(InformationSubmission::getPayload, SubmittedInformationDTO::setPayload)); + } + + private JsonNode payloadConverter(String jsonPayload) throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + if (jsonPayload == null) { + jsonPayload = "{}"; + } + return mapper.readTree(jsonPayload); + } +} diff --git a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionService.java b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionService.java new file mode 100644 index 000000000..1769ac52b --- /dev/null +++ b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionService.java @@ -0,0 +1,13 @@ +package eu.bbmri_eric.negotiator.service; + +import eu.bbmri_eric.negotiator.dto.InformationSubmissionDTO; +import eu.bbmri_eric.negotiator.dto.SubmittedInformationDTO; + +public interface InformationSubmissionService { + SubmittedInformationDTO submit( + InformationSubmissionDTO informationSubmissionDTO, + Long informationRequirementId, + String negotiationId); + + SubmittedInformationDTO findById(Long id); +} diff --git a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java new file mode 100644 index 000000000..1077b5d1b --- /dev/null +++ b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java @@ -0,0 +1,77 @@ +package eu.bbmri_eric.negotiator.service; + +import eu.bbmri_eric.negotiator.database.model.InformationRequirement; +import eu.bbmri_eric.negotiator.database.model.InformationSubmission; +import eu.bbmri_eric.negotiator.database.model.Negotiation; +import eu.bbmri_eric.negotiator.database.model.Resource; +import eu.bbmri_eric.negotiator.database.repository.InformationRequirementRepository; +import eu.bbmri_eric.negotiator.database.repository.InformationSubmissionRepository; +import eu.bbmri_eric.negotiator.database.repository.NegotiationRepository; +import eu.bbmri_eric.negotiator.database.repository.ResourceRepository; +import eu.bbmri_eric.negotiator.dto.InformationSubmissionDTO; +import eu.bbmri_eric.negotiator.dto.SubmittedInformationDTO; +import eu.bbmri_eric.negotiator.exceptions.EntityNotFoundException; +import jakarta.transaction.Transactional; +import lombok.extern.apachecommons.CommonsLog; +import org.modelmapper.ModelMapper; +import org.springframework.stereotype.Service; + +@Service +@Transactional +@CommonsLog +public class InformationSubmissionServiceImpl implements InformationSubmissionService { + + private final InformationSubmissionRepository informationSubmissionRepository; + private final InformationRequirementRepository informationRequirementRepository; + private final ResourceRepository resourceRepository; + private final NegotiationRepository negotiationRepository; + private final ModelMapper modelMapper; + + public InformationSubmissionServiceImpl( + InformationSubmissionRepository informationSubmissionRepository, + InformationRequirementRepository informationRequirementRepository, + ResourceRepository resourceRepository, + NegotiationRepository negotiationRepository, + ModelMapper modelMapper) { + this.informationSubmissionRepository = informationSubmissionRepository; + this.informationRequirementRepository = informationRequirementRepository; + this.resourceRepository = resourceRepository; + this.negotiationRepository = negotiationRepository; + this.modelMapper = modelMapper; + } + + @Override + public SubmittedInformationDTO submit( + InformationSubmissionDTO informationSubmissionDTO, + Long informationRequirementId, + String negotiationId) { + InformationRequirement requirement = + informationRequirementRepository + .findById(informationRequirementId) + .orElseThrow(() -> new EntityNotFoundException(informationRequirementId)); + Resource resource = + resourceRepository + .findById(informationSubmissionDTO.getResourceId()) + .orElseThrow( + () -> new EntityNotFoundException(informationSubmissionDTO.getResourceId())); + Negotiation negotiation = + negotiationRepository + .findById(negotiationId) + .orElseThrow(() -> new EntityNotFoundException(negotiationId)); + InformationSubmission submission = + new InformationSubmission( + requirement, resource, negotiation, informationSubmissionDTO.getPayload().toString()); + log.info(submission.getPayload()); + return modelMapper.map( + informationSubmissionRepository.save(submission), SubmittedInformationDTO.class); + } + + @Override + public SubmittedInformationDTO findById(Long id) { + return modelMapper.map( + informationSubmissionRepository + .findById(id) + .orElseThrow(() -> new EntityNotFoundException(id)), + SubmittedInformationDTO.class); + } +} diff --git a/src/main/resources/db/migration/postgresql/V12.0__add_info_submission.sql b/src/main/resources/db/migration/postgresql/V12.0__add_info_submission.sql new file mode 100644 index 000000000..690ba894f --- /dev/null +++ b/src/main/resources/db/migration/postgresql/V12.0__add_info_submission.sql @@ -0,0 +1,14 @@ +CREATE TABLE information_submission +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + requirement_id BIGINT, + resource_id BIGINT, + payload JSONB, + CONSTRAINT pk_informationsubmission PRIMARY KEY (id) +); + +ALTER TABLE information_submission + ADD CONSTRAINT FK_INFORMATIONSUBMISSION_ON_REQUIREMENT FOREIGN KEY (requirement_id) REFERENCES information_requirement (id); + +ALTER TABLE information_submission + ADD CONSTRAINT FK_INFORMATIONSUBMISSION_ON_RESOURCE FOREIGN KEY (resource_id) REFERENCES resource (id); \ No newline at end of file diff --git a/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java b/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java index a661231c9..c556c93d3 100644 --- a/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java +++ b/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java @@ -37,6 +37,7 @@ public class InformationRequirementControllerTest { private final String INFO_REQUIREMENT_ENDPOINT = "/v3/info-requirements"; private final String INFO_SUBMISSION_ENDPOINT = "/v3/negotiations/%s/info-requirements/%s"; + private final String SUBMISSION_ENDPOINT = "/v3/info-submissions/%s"; private MockMvc mockMvc; @Autowired private NegotiationRepository negotiationRepository; @Autowired private InformationRequirementRepository informationRequirementRepository; @@ -279,7 +280,7 @@ void submitInformation_correctPayload_ok() throws Exception { """; ObjectMapper mapper = new ObjectMapper(); JsonNode jsonPayload = mapper.readTree(payload); - InformationSubmissionDTO submissionDTO = new InformationSubmissionDTO(1L, jsonPayload); + InformationSubmissionDTO submissionDTO = new InformationSubmissionDTO(4L, jsonPayload); mockMvc .perform( MockMvcRequestBuilders.post( @@ -292,4 +293,11 @@ void submitInformation_correctPayload_ok() throws Exception { .andExpect(jsonPath("$.resourceId").value(submissionDTO.getResourceId())) .andExpect(jsonPath("$.payload.sample-type").value("DNA")); } + + @Test + void getSubmission_exists_ok() throws Exception { + mockMvc + .perform(MockMvcRequestBuilders.get(SUBMISSION_ENDPOINT.formatted("1"))) + .andExpect(status().isOk()); + } } From 8d156e128cabeed25c4601eaf38f1b540e4083d4 Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Thu, 1 Aug 2024 11:30:53 +0200 Subject: [PATCH 03/24] feat(info-submission): add functionality for submitting required information Signed-off-by: RadovanTomik --- .../InformationSubmissionRepository.java | 7 ++- .../events/InformationSubmissionEvent.java | 14 ++++++ .../service/InformationSubmissionService.java | 23 ++++++++++ .../InformationSubmissionServiceImpl.java | 20 ++++++++- .../postgresql/V12.0__add_info_submission.sql | 4 ++ .../InformationRequirementControllerTest.java | 45 ++++++++++++++++++- 6 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 src/main/java/eu/bbmri_eric/negotiator/events/InformationSubmissionEvent.java diff --git a/src/main/java/eu/bbmri_eric/negotiator/database/repository/InformationSubmissionRepository.java b/src/main/java/eu/bbmri_eric/negotiator/database/repository/InformationSubmissionRepository.java index fd42f5b21..5ceb8a051 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/database/repository/InformationSubmissionRepository.java +++ b/src/main/java/eu/bbmri_eric/negotiator/database/repository/InformationSubmissionRepository.java @@ -1,7 +1,12 @@ package eu.bbmri_eric.negotiator.database.repository; import eu.bbmri_eric.negotiator.database.model.InformationSubmission; +import java.util.Set; import org.springframework.data.jpa.repository.JpaRepository; public interface InformationSubmissionRepository - extends JpaRepository {} + extends JpaRepository { + boolean existsByResource_SourceIdAndNegotiation_Id(String sourceId, String negotiationId); + + Set findAllByNegotiation_Id(String negotiationId); +} diff --git a/src/main/java/eu/bbmri_eric/negotiator/events/InformationSubmissionEvent.java b/src/main/java/eu/bbmri_eric/negotiator/events/InformationSubmissionEvent.java new file mode 100644 index 000000000..c3481cb4f --- /dev/null +++ b/src/main/java/eu/bbmri_eric/negotiator/events/InformationSubmissionEvent.java @@ -0,0 +1,14 @@ +package eu.bbmri_eric.negotiator.events; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class InformationSubmissionEvent extends ApplicationEvent { + private final String negotiationId; + + public InformationSubmissionEvent(Object source, String negotiationId) { + super(source); + this.negotiationId = negotiationId; + } +} diff --git a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionService.java b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionService.java index 1769ac52b..58c7137b8 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionService.java +++ b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionService.java @@ -2,12 +2,35 @@ import eu.bbmri_eric.negotiator.dto.InformationSubmissionDTO; import eu.bbmri_eric.negotiator.dto.SubmittedInformationDTO; +import java.util.List; public interface InformationSubmissionService { + /** + * Submit required Information for the given resource in a Negotiation. + * + * @param informationSubmissionDTO a DTO containing necessary information + * @param informationRequirementId id of the requirement + * @param negotiationId id of the negotiation + * @return submitted information + */ SubmittedInformationDTO submit( InformationSubmissionDTO informationSubmissionDTO, Long informationRequirementId, String negotiationId); + /** + * Find an information submission by ID. + * + * @param id the ID of the sought submission + * @return the submitted information + */ SubmittedInformationDTO findById(Long id); + + /** + * Find all submissions for a given Negotiation. + * + * @param negotiationId the ID of the Negotiation + * @return a list of all linked submissions + */ + List findAllForNegotiation(String negotiationId); } diff --git a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java index 1077b5d1b..21f134c9f 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java +++ b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java @@ -10,10 +10,13 @@ import eu.bbmri_eric.negotiator.database.repository.ResourceRepository; import eu.bbmri_eric.negotiator.dto.InformationSubmissionDTO; import eu.bbmri_eric.negotiator.dto.SubmittedInformationDTO; +import eu.bbmri_eric.negotiator.events.InformationSubmissionEvent; import eu.bbmri_eric.negotiator.exceptions.EntityNotFoundException; import jakarta.transaction.Transactional; +import java.util.List; import lombok.extern.apachecommons.CommonsLog; import org.modelmapper.ModelMapper; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; @Service @@ -26,18 +29,21 @@ public class InformationSubmissionServiceImpl implements InformationSubmissionSe private final ResourceRepository resourceRepository; private final NegotiationRepository negotiationRepository; private final ModelMapper modelMapper; + private final ApplicationEventPublisher applicationEventPublisher; public InformationSubmissionServiceImpl( InformationSubmissionRepository informationSubmissionRepository, InformationRequirementRepository informationRequirementRepository, ResourceRepository resourceRepository, NegotiationRepository negotiationRepository, - ModelMapper modelMapper) { + ModelMapper modelMapper, + ApplicationEventPublisher applicationEventPublisher) { this.informationSubmissionRepository = informationSubmissionRepository; this.informationRequirementRepository = informationRequirementRepository; this.resourceRepository = resourceRepository; this.negotiationRepository = negotiationRepository; this.modelMapper = modelMapper; + this.applicationEventPublisher = applicationEventPublisher; } @Override @@ -62,8 +68,9 @@ public SubmittedInformationDTO submit( new InformationSubmission( requirement, resource, negotiation, informationSubmissionDTO.getPayload().toString()); log.info(submission.getPayload()); + applicationEventPublisher.publishEvent(new InformationSubmissionEvent(this, negotiationId)); return modelMapper.map( - informationSubmissionRepository.save(submission), SubmittedInformationDTO.class); + informationSubmissionRepository.saveAndFlush(submission), SubmittedInformationDTO.class); } @Override @@ -74,4 +81,13 @@ public SubmittedInformationDTO findById(Long id) { .orElseThrow(() -> new EntityNotFoundException(id)), SubmittedInformationDTO.class); } + + @Override + public List findAllForNegotiation(String negotiationId) { + return informationSubmissionRepository.findAllByNegotiation_Id(negotiationId).stream() + .map( + informationSubmission -> + modelMapper.map(informationSubmission, SubmittedInformationDTO.class)) + .toList(); + } } diff --git a/src/main/resources/db/migration/postgresql/V12.0__add_info_submission.sql b/src/main/resources/db/migration/postgresql/V12.0__add_info_submission.sql index 690ba894f..9bbb57bc2 100644 --- a/src/main/resources/db/migration/postgresql/V12.0__add_info_submission.sql +++ b/src/main/resources/db/migration/postgresql/V12.0__add_info_submission.sql @@ -3,10 +3,14 @@ CREATE TABLE information_submission id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, requirement_id BIGINT, resource_id BIGINT, + negotiation_id VARCHAR(255), payload JSONB, CONSTRAINT pk_informationsubmission PRIMARY KEY (id) ); +ALTER TABLE information_submission + ADD CONSTRAINT FK_INFORMATIONSUBMISSION_ON_NEGOTIATION FOREIGN KEY (negotiation_id) REFERENCES negotiation (id); + ALTER TABLE information_submission ADD CONSTRAINT FK_INFORMATIONSUBMISSION_ON_REQUIREMENT FOREIGN KEY (requirement_id) REFERENCES information_requirement (id); diff --git a/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java b/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java index c556c93d3..5a7905ccf 100644 --- a/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java +++ b/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java @@ -16,6 +16,7 @@ import eu.bbmri_eric.negotiator.dto.InformationRequirementDTO; import eu.bbmri_eric.negotiator.dto.InformationSubmissionDTO; import eu.bbmri_eric.negotiator.service.InformationRequirementServiceImpl; +import jakarta.transaction.Transactional; import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -264,6 +265,7 @@ void getSummary_noSubmissions_emptyResponse() throws Exception { } @Test + @Transactional void submitInformation_correctPayload_ok() throws Exception { Negotiation negotiation = negotiationRepository.findAll().iterator().next(); InformationRequirementDTO informationRequirementDTO = @@ -280,7 +282,9 @@ void submitInformation_correctPayload_ok() throws Exception { """; ObjectMapper mapper = new ObjectMapper(); JsonNode jsonPayload = mapper.readTree(payload); - InformationSubmissionDTO submissionDTO = new InformationSubmissionDTO(4L, jsonPayload); + InformationSubmissionDTO submissionDTO = + new InformationSubmissionDTO( + negotiation.getResources().iterator().next().getId(), jsonPayload); mockMvc .perform( MockMvcRequestBuilders.post( @@ -295,9 +299,46 @@ void submitInformation_correctPayload_ok() throws Exception { } @Test + @Transactional void getSubmission_exists_ok() throws Exception { + Negotiation negotiation = negotiationRepository.findAll().iterator().next(); + InformationRequirementDTO informationRequirementDTO = + informationRequirementServiceImpl.createInformationRequirement( + new InformationRequirementCreateDTO(1L, NegotiationResourceEvent.CONTACT)); + String payload = + """ + { + "sample-type": "DNA", + "num-of-subjects": 10, + "num-of-samples": 20, + "volume-per-sample": 5 + } + """; + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonPayload = mapper.readTree(payload); + InformationSubmissionDTO submissionDTO = + new InformationSubmissionDTO( + negotiation.getResources().iterator().next().getId(), jsonPayload); + MvcResult mvcResult = + mockMvc + .perform( + MockMvcRequestBuilders.post( + INFO_SUBMISSION_ENDPOINT.formatted( + negotiation.getId(), informationRequirementDTO.getId())) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(submissionDTO))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").isNumber()) + .andExpect(jsonPath("$.resourceId").value(submissionDTO.getResourceId())) + .andExpect(jsonPath("$.payload.sample-type").value("DNA")) + .andReturn(); + long id = + new ObjectMapper() + .readTree(mvcResult.getResponse().getContentAsString()) + .get("id") + .asLong(); mockMvc - .perform(MockMvcRequestBuilders.get(SUBMISSION_ENDPOINT.formatted("1"))) + .perform(MockMvcRequestBuilders.get(SUBMISSION_ENDPOINT.formatted(id))) .andExpect(status().isOk()); } } From 8e386a581b44efade5063a26be087ddcbd8fe135 Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Thu, 1 Aug 2024 11:34:18 +0200 Subject: [PATCH 04/24] fix(info-submission): rename db script Signed-off-by: RadovanTomik --- ....0__add_info_submission.sql => V13.0__add_info_submission.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/postgresql/{V12.0__add_info_submission.sql => V13.0__add_info_submission.sql} (100%) diff --git a/src/main/resources/db/migration/postgresql/V12.0__add_info_submission.sql b/src/main/resources/db/migration/postgresql/V13.0__add_info_submission.sql similarity index 100% rename from src/main/resources/db/migration/postgresql/V12.0__add_info_submission.sql rename to src/main/resources/db/migration/postgresql/V13.0__add_info_submission.sql From b3b68edf0ed83fc243a7fb617d5a111fefc057d7 Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Thu, 1 Aug 2024 12:17:39 +0200 Subject: [PATCH 05/24] chore(info-submission): code style Signed-off-by: RadovanTomik --- .../api/controller/v3/InformationRequirementController.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationRequirementController.java b/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationRequirementController.java index 76bdbaea4..3fa11f3bb 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationRequirementController.java +++ b/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationRequirementController.java @@ -75,6 +75,4 @@ public EntityModel updateRequirement( public void deleteRequirement(@PathVariable Long id) { service.deleteInformationRequirement(id); } - - } From b1fe0efec0f711604cfcfe5e7f50f320949fd5c6 Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Thu, 1 Aug 2024 12:20:17 +0200 Subject: [PATCH 06/24] chore(info-submission): code style Signed-off-by: RadovanTomik --- .github/workflows/CI.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 411313ba7..48a7ef8c0 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -65,6 +65,11 @@ jobs: steps: + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' - name: Checkout Code uses: actions/checkout@v4 From 87a95c2b417335428ec8472960c65f63b2f039ef Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Thu, 1 Aug 2024 12:37:11 +0200 Subject: [PATCH 07/24] tests(info-submission): add unit tests for entity Signed-off-by: RadovanTomik --- .../unit/model/InformationSubmissionTest.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/test/java/eu/bbmri_eric/negotiator/unit/model/InformationSubmissionTest.java diff --git a/src/test/java/eu/bbmri_eric/negotiator/unit/model/InformationSubmissionTest.java b/src/test/java/eu/bbmri_eric/negotiator/unit/model/InformationSubmissionTest.java new file mode 100644 index 000000000..5e40efb51 --- /dev/null +++ b/src/test/java/eu/bbmri_eric/negotiator/unit/model/InformationSubmissionTest.java @@ -0,0 +1,51 @@ +package eu.bbmri_eric.negotiator.unit.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import eu.bbmri_eric.negotiator.database.model.InformationRequirement; +import eu.bbmri_eric.negotiator.database.model.InformationSubmission; +import eu.bbmri_eric.negotiator.database.model.Negotiation; +import eu.bbmri_eric.negotiator.database.model.Resource; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class InformationSubmissionTest { + @Test + public void testPublicConstructor() { + InformationRequirement requirement = Mockito.mock(InformationRequirement.class); + Resource resource = Mockito.mock(Resource.class); + Negotiation negotiation = Mockito.mock(Negotiation.class); + String payload = "Test Payload"; + + InformationSubmission infoSub = new InformationSubmission(requirement, resource, negotiation, payload); + assertNull(infoSub.getId()); + assertEquals(requirement, infoSub.getRequirement()); + assertEquals(resource, infoSub.getResource()); + assertEquals(negotiation, infoSub.getNegotiation()); + assertEquals(payload, infoSub.getPayload()); + } + + @Test + public void testSettersAndGetters() { + InformationRequirement requirement = Mockito.mock(InformationRequirement.class); + Resource resource = Mockito.mock(Resource.class); + Negotiation negotiation = Mockito.mock(Negotiation.class); + String payload = "Test Payload"; + + InformationSubmission infoSub = new InformationSubmission(requirement, resource, negotiation, payload); + Long id = 1L; + + infoSub.setId(id); + infoSub.setRequirement(requirement); + infoSub.setResource(resource); + infoSub.setNegotiation(negotiation); + infoSub.setPayload(payload); + + assertEquals(id, infoSub.getId()); + assertEquals(requirement, infoSub.getRequirement()); + assertEquals(resource, infoSub.getResource()); + assertEquals(negotiation, infoSub.getNegotiation()); + assertEquals(payload, infoSub.getPayload()); + } +} From 8dbf606666ec4259d1e785a8ba25ba2179afe5a4 Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Thu, 1 Aug 2024 13:21:01 +0200 Subject: [PATCH 08/24] tests(info-submission): add unit tests for mapper Signed-off-by: RadovanTomik --- .../InformationSubmissionMapperTest.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/test/java/eu/bbmri_eric/negotiator/unit/mappers/InformationSubmissionMapperTest.java diff --git a/src/test/java/eu/bbmri_eric/negotiator/unit/mappers/InformationSubmissionMapperTest.java b/src/test/java/eu/bbmri_eric/negotiator/unit/mappers/InformationSubmissionMapperTest.java new file mode 100644 index 000000000..27c861e85 --- /dev/null +++ b/src/test/java/eu/bbmri_eric/negotiator/unit/mappers/InformationSubmissionMapperTest.java @@ -0,0 +1,65 @@ +package eu.bbmri_eric.negotiator.unit.mappers; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.bbmri_eric.negotiator.configuration.state_machine.resource.NegotiationResourceEvent; +import eu.bbmri_eric.negotiator.database.model.AccessForm; +import eu.bbmri_eric.negotiator.database.model.InformationRequirement; +import eu.bbmri_eric.negotiator.database.model.InformationSubmission; +import eu.bbmri_eric.negotiator.dto.SubmittedInformationDTO; +import eu.bbmri_eric.negotiator.mappers.InformationSubmissionMapper; +import java.io.IOException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.modelmapper.ModelMapper; + +public class InformationSubmissionMapperTest { + private ModelMapper modelMapper; + + @BeforeEach + public void setUp() { + modelMapper = new ModelMapper(); + InformationSubmissionMapper informationSubmissionMapper = + new InformationSubmissionMapper(modelMapper); + informationSubmissionMapper.addMappings(); + } + + @Test + void testMappings_normalJson_ok() throws IOException { + String content = + """ + { + "sample-type": "DNA", + "num-of-subjects": 10, + "num-of-samples": 20, + "volume-per-sample": 5 + } + """; + JsonNode payload = new ObjectMapper().readTree(content); + InformationRequirement requirement = + new InformationRequirement(1L, new AccessForm("test"), NegotiationResourceEvent.CONTACT); + InformationSubmission entity = new InformationSubmission(requirement, null, null, content); + assertEquals(payload, modelMapper.map(entity, SubmittedInformationDTO.class).getPayload()); + } + + @Test + void testMappings_nullJson_emptyJson() throws IOException { + InformationRequirement requirement = + new InformationRequirement(1L, new AccessForm("test"), NegotiationResourceEvent.CONTACT); + InformationSubmission entity = new InformationSubmission(requirement, null, null, null); + assertEquals( + new ObjectMapper().readTree("{}"), + modelMapper.map(entity, SubmittedInformationDTO.class).getPayload()); + } + + @Test + void testMappings_nullArguments_equalsNull() { + InformationRequirement requirement = + new InformationRequirement(1L, new AccessForm("test"), NegotiationResourceEvent.CONTACT); + InformationSubmission entity = new InformationSubmission(requirement, null, null, null); + assertEquals(null, modelMapper.map(entity, SubmittedInformationDTO.class).getResourceId()); + assertEquals(null, modelMapper.map(entity, SubmittedInformationDTO.class).getId()); + } +} From 916cd0ae7d5a3861d6f3785d4935dcf105998d10 Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Thu, 1 Aug 2024 13:25:24 +0200 Subject: [PATCH 09/24] refactor(info-submission): refactor InformationSubmissionService Signed-off-by: RadovanTomik --- .../InformationSubmissionServiceImpl.java | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java index 21f134c9f..e45322312 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java +++ b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java @@ -14,6 +14,7 @@ import eu.bbmri_eric.negotiator.exceptions.EntityNotFoundException; import jakarta.transaction.Transactional; import java.util.List; +import lombok.NonNull; import lombok.extern.apachecommons.CommonsLog; import org.modelmapper.ModelMapper; import org.springframework.context.ApplicationEventPublisher; @@ -51,6 +52,17 @@ public SubmittedInformationDTO submit( InformationSubmissionDTO informationSubmissionDTO, Long informationRequirementId, String negotiationId) { + InformationSubmission submission = + buildSubmissionEntity(informationSubmissionDTO, informationRequirementId, negotiationId); + submission = informationSubmissionRepository.saveAndFlush(submission); + applicationEventPublisher.publishEvent(new InformationSubmissionEvent(this, negotiationId)); + return modelMapper.map(submission, SubmittedInformationDTO.class); + } + + private @NonNull InformationSubmission buildSubmissionEntity( + InformationSubmissionDTO informationSubmissionDTO, + Long informationRequirementId, + String negotiationId) { InformationRequirement requirement = informationRequirementRepository .findById(informationRequirementId) @@ -64,13 +76,8 @@ public SubmittedInformationDTO submit( negotiationRepository .findById(negotiationId) .orElseThrow(() -> new EntityNotFoundException(negotiationId)); - InformationSubmission submission = - new InformationSubmission( - requirement, resource, negotiation, informationSubmissionDTO.getPayload().toString()); - log.info(submission.getPayload()); - applicationEventPublisher.publishEvent(new InformationSubmissionEvent(this, negotiationId)); - return modelMapper.map( - informationSubmissionRepository.saveAndFlush(submission), SubmittedInformationDTO.class); + return new InformationSubmission( + requirement, resource, negotiation, informationSubmissionDTO.getPayload().toString()); } @Override From 5c47eb8993fe585c76c9c37f817067eb3bc3b814 Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Thu, 1 Aug 2024 14:31:26 +0200 Subject: [PATCH 10/24] feat(info-submission): add access control Signed-off-by: RadovanTomik --- .../security/OAuthSecurityConfig.java | 2 + .../InformationSubmissionServiceImpl.java | 42 +++++++- .../InformationRequirementControllerTest.java | 100 +++++++++++++++++- 3 files changed, 136 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/bbmri_eric/negotiator/configuration/security/OAuthSecurityConfig.java b/src/main/java/eu/bbmri_eric/negotiator/configuration/security/OAuthSecurityConfig.java index 0a9a01e26..1f9f1598f 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/configuration/security/OAuthSecurityConfig.java +++ b/src/main/java/eu/bbmri_eric/negotiator/configuration/security/OAuthSecurityConfig.java @@ -179,6 +179,8 @@ public SecurityFilterChain filterChain(HttpSecurity http, MvcRequestMatcher.Buil .hasRole("ADMIN") .requestMatchers(mvc.pattern(HttpMethod.DELETE, "/v3/info-requirements/**")) .hasRole("ADMIN") + .requestMatchers(mvc.pattern("/v3/info-submissions/**")) + .authenticated() .requestMatchers(mvc.pattern("/actuator/prometheus")) // Needs to be IPv6 address .access( diff --git a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java index e45322312..452df0b30 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java +++ b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java @@ -1,19 +1,24 @@ package eu.bbmri_eric.negotiator.service; +import eu.bbmri_eric.negotiator.configuration.security.auth.NegotiatorUserDetailsService; import eu.bbmri_eric.negotiator.database.model.InformationRequirement; import eu.bbmri_eric.negotiator.database.model.InformationSubmission; import eu.bbmri_eric.negotiator.database.model.Negotiation; +import eu.bbmri_eric.negotiator.database.model.Person; import eu.bbmri_eric.negotiator.database.model.Resource; import eu.bbmri_eric.negotiator.database.repository.InformationRequirementRepository; import eu.bbmri_eric.negotiator.database.repository.InformationSubmissionRepository; import eu.bbmri_eric.negotiator.database.repository.NegotiationRepository; +import eu.bbmri_eric.negotiator.database.repository.PersonRepository; import eu.bbmri_eric.negotiator.database.repository.ResourceRepository; import eu.bbmri_eric.negotiator.dto.InformationSubmissionDTO; import eu.bbmri_eric.negotiator.dto.SubmittedInformationDTO; import eu.bbmri_eric.negotiator.events.InformationSubmissionEvent; import eu.bbmri_eric.negotiator.exceptions.EntityNotFoundException; +import eu.bbmri_eric.negotiator.exceptions.ForbiddenRequestException; import jakarta.transaction.Transactional; import java.util.List; +import java.util.Optional; import lombok.NonNull; import lombok.extern.apachecommons.CommonsLog; import org.modelmapper.ModelMapper; @@ -31,6 +36,7 @@ public class InformationSubmissionServiceImpl implements InformationSubmissionSe private final NegotiationRepository negotiationRepository; private final ModelMapper modelMapper; private final ApplicationEventPublisher applicationEventPublisher; + private final PersonRepository personRepository; public InformationSubmissionServiceImpl( InformationSubmissionRepository informationSubmissionRepository, @@ -38,13 +44,15 @@ public InformationSubmissionServiceImpl( ResourceRepository resourceRepository, NegotiationRepository negotiationRepository, ModelMapper modelMapper, - ApplicationEventPublisher applicationEventPublisher) { + ApplicationEventPublisher applicationEventPublisher, + PersonRepository personRepository) { this.informationSubmissionRepository = informationSubmissionRepository; this.informationRequirementRepository = informationRequirementRepository; this.resourceRepository = resourceRepository; this.negotiationRepository = negotiationRepository; this.modelMapper = modelMapper; this.applicationEventPublisher = applicationEventPublisher; + this.personRepository = personRepository; } @Override @@ -52,6 +60,11 @@ public SubmittedInformationDTO submit( InformationSubmissionDTO informationSubmissionDTO, Long informationRequirementId, String negotiationId) { + if (!isAuthorizedToWrite( + NegotiatorUserDetailsService.getCurrentlyAuthenticatedUserInternalId(), + informationSubmissionDTO.getResourceId())) { + throw new ForbiddenRequestException("You are not authorized to perform this action"); + } InformationSubmission submission = buildSubmissionEntity(informationSubmissionDTO, informationRequirementId, negotiationId); submission = informationSubmissionRepository.saveAndFlush(submission); @@ -59,6 +72,21 @@ public SubmittedInformationDTO submit( return modelMapper.map(submission, SubmittedInformationDTO.class); } + private boolean isAuthorizedToWrite(Long personId, Long resourceId) { + Optional personOpt = personRepository.findById(personId); + if (personOpt.isPresent()) { + Person person = personOpt.get(); + return person.getResources().stream() + .anyMatch(resource -> resource.getId().equals(resourceId)); + } + return false; + } + + private boolean isAuthorizedToRead(String negotiationId, Long personId, Long resourceId) { + return isAuthorizedToWrite(personId, resourceId) + || negotiationRepository.existsByIdAndCreatedBy_Id(negotiationId, personId); + } + private @NonNull InformationSubmission buildSubmissionEntity( InformationSubmissionDTO informationSubmissionDTO, Long informationRequirementId, @@ -82,11 +110,17 @@ public SubmittedInformationDTO submit( @Override public SubmittedInformationDTO findById(Long id) { - return modelMapper.map( + InformationSubmission submission = informationSubmissionRepository .findById(id) - .orElseThrow(() -> new EntityNotFoundException(id)), - SubmittedInformationDTO.class); + .orElseThrow(() -> new EntityNotFoundException(id)); + if (!isAuthorizedToRead( + submission.getNegotiation().getId(), + NegotiatorUserDetailsService.getCurrentlyAuthenticatedUserInternalId(), + submission.getResource().getId())) { + throw new ForbiddenRequestException("You are not authorized to perform this action"); + } + return modelMapper.map(submission, SubmittedInformationDTO.class); } @Override diff --git a/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java b/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java index 5a7905ccf..364134e04 100644 --- a/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java +++ b/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java @@ -1,6 +1,7 @@ package eu.bbmri_eric.negotiator.integration.api.v3; import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -8,9 +9,12 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import eu.bbmri_eric.negotiator.NegotiatorApplication; +import eu.bbmri_eric.negotiator.configuration.security.auth.NegotiatorUserDetailsService; import eu.bbmri_eric.negotiator.configuration.state_machine.resource.NegotiationResourceEvent; +import eu.bbmri_eric.negotiator.database.model.InformationSubmission; import eu.bbmri_eric.negotiator.database.model.Negotiation; import eu.bbmri_eric.negotiator.database.repository.InformationRequirementRepository; +import eu.bbmri_eric.negotiator.database.repository.InformationSubmissionRepository; import eu.bbmri_eric.negotiator.database.repository.NegotiationRepository; import eu.bbmri_eric.negotiator.dto.InformationRequirementCreateDTO; import eu.bbmri_eric.negotiator.dto.InformationRequirementDTO; @@ -24,6 +28,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; @@ -42,6 +47,7 @@ public class InformationRequirementControllerTest { private MockMvc mockMvc; @Autowired private NegotiationRepository negotiationRepository; @Autowired private InformationRequirementRepository informationRequirementRepository; + @Autowired private InformationSubmissionRepository informationSubmissionRepository; @Autowired private InformationRequirementServiceImpl informationRequirementServiceImpl; @BeforeEach @@ -265,6 +271,7 @@ void getSummary_noSubmissions_emptyResponse() throws Exception { } @Test + @WithUserDetails("TheBiobanker") @Transactional void submitInformation_correctPayload_ok() throws Exception { Negotiation negotiation = negotiationRepository.findAll().iterator().next(); @@ -299,8 +306,95 @@ void submitInformation_correctPayload_ok() throws Exception { } @Test + @WithUserDetails("researcher") @Transactional - void getSubmission_exists_ok() throws Exception { + void getSubmission_existsButNotAuth_403() throws Exception { + Negotiation negotiation = negotiationRepository.findAll().iterator().next(); + InformationSubmission informationSubmission = + informationSubmissionRepository.saveAndFlush( + new InformationSubmission( + null, negotiation.getResources().iterator().next(), negotiation, "{}")); + mockMvc + .perform( + MockMvcRequestBuilders.get( + SUBMISSION_ENDPOINT.formatted(informationSubmission.getId()))) + .andExpect(status().isForbidden()); + } + + @Test + @WithUserDetails("TheResearcher") + @Transactional + void getSubmission_existsAsRequestAuthor_ok() throws Exception { + Negotiation negotiation = negotiationRepository.findById("negotiation-1").get(); + assertEquals( + negotiation.getCreatedBy().getId(), + NegotiatorUserDetailsService.getCurrentlyAuthenticatedUserInternalId()); + InformationSubmission informationSubmission = + informationSubmissionRepository.saveAndFlush( + new InformationSubmission( + null, negotiation.getResources().iterator().next(), negotiation, "{}")); + mockMvc + .perform( + MockMvcRequestBuilders.get( + SUBMISSION_ENDPOINT.formatted(informationSubmission.getId()))) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("TheResearcher") + @Transactional + void getSubmission_existsAsRepresentative_ok() throws Exception { + Negotiation negotiation = negotiationRepository.findById("negotiation-1").get(); + InformationSubmission informationSubmission = + informationSubmissionRepository.saveAndFlush( + new InformationSubmission( + null, + negotiation.getResources().stream() + .filter(resource -> resource.getId().equals(4L)) + .findFirst() + .get(), + negotiation, + "{}")); + mockMvc + .perform( + MockMvcRequestBuilders.get( + SUBMISSION_ENDPOINT.formatted(informationSubmission.getId()))) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("TheResearcher") + @Transactional + void submit_notARepresentative_403() throws Exception { + Negotiation negotiation = negotiationRepository.findAll().iterator().next(); + InformationRequirementDTO informationRequirementDTO = + informationRequirementServiceImpl.createInformationRequirement( + new InformationRequirementCreateDTO(1L, NegotiationResourceEvent.CONTACT)); + String payload = + """ + { + "sample-type": "DNA", + "num-of-subjects": 10, + "num-of-samples": 20, + "volume-per-sample": 5 + } + """; + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonPayload = mapper.readTree(payload); + InformationSubmissionDTO submissionDTO = + new InformationSubmissionDTO( + negotiation.getResources().iterator().next().getId(), jsonPayload); + mockMvc + .perform( + MockMvcRequestBuilders.post( + INFO_SUBMISSION_ENDPOINT.formatted( + negotiation.getId(), informationRequirementDTO.getId())) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(submissionDTO))) + .andExpect(status().isForbidden()); + } + + private long createInformationSubmission() throws Exception { Negotiation negotiation = negotiationRepository.findAll().iterator().next(); InformationRequirementDTO informationRequirementDTO = informationRequirementServiceImpl.createInformationRequirement( @@ -337,8 +431,6 @@ void getSubmission_exists_ok() throws Exception { .readTree(mvcResult.getResponse().getContentAsString()) .get("id") .asLong(); - mockMvc - .perform(MockMvcRequestBuilders.get(SUBMISSION_ENDPOINT.formatted(id))) - .andExpect(status().isOk()); + return id; } } From f48f434555854f9a2c1b983acf134cf2dd4b51b8 Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Thu, 1 Aug 2024 14:32:44 +0200 Subject: [PATCH 11/24] feat(info-submission): add access control Signed-off-by: RadovanTomik --- .../unit/model/InformationSubmissionTest.java | 76 ++++++++++--------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/src/test/java/eu/bbmri_eric/negotiator/unit/model/InformationSubmissionTest.java b/src/test/java/eu/bbmri_eric/negotiator/unit/model/InformationSubmissionTest.java index 5e40efb51..782665a8c 100644 --- a/src/test/java/eu/bbmri_eric/negotiator/unit/model/InformationSubmissionTest.java +++ b/src/test/java/eu/bbmri_eric/negotiator/unit/model/InformationSubmissionTest.java @@ -11,41 +11,43 @@ import org.mockito.Mockito; public class InformationSubmissionTest { - @Test - public void testPublicConstructor() { - InformationRequirement requirement = Mockito.mock(InformationRequirement.class); - Resource resource = Mockito.mock(Resource.class); - Negotiation negotiation = Mockito.mock(Negotiation.class); - String payload = "Test Payload"; - - InformationSubmission infoSub = new InformationSubmission(requirement, resource, negotiation, payload); - assertNull(infoSub.getId()); - assertEquals(requirement, infoSub.getRequirement()); - assertEquals(resource, infoSub.getResource()); - assertEquals(negotiation, infoSub.getNegotiation()); - assertEquals(payload, infoSub.getPayload()); - } - - @Test - public void testSettersAndGetters() { - InformationRequirement requirement = Mockito.mock(InformationRequirement.class); - Resource resource = Mockito.mock(Resource.class); - Negotiation negotiation = Mockito.mock(Negotiation.class); - String payload = "Test Payload"; - - InformationSubmission infoSub = new InformationSubmission(requirement, resource, negotiation, payload); - Long id = 1L; - - infoSub.setId(id); - infoSub.setRequirement(requirement); - infoSub.setResource(resource); - infoSub.setNegotiation(negotiation); - infoSub.setPayload(payload); - - assertEquals(id, infoSub.getId()); - assertEquals(requirement, infoSub.getRequirement()); - assertEquals(resource, infoSub.getResource()); - assertEquals(negotiation, infoSub.getNegotiation()); - assertEquals(payload, infoSub.getPayload()); - } + @Test + public void testPublicConstructor() { + InformationRequirement requirement = Mockito.mock(InformationRequirement.class); + Resource resource = Mockito.mock(Resource.class); + Negotiation negotiation = Mockito.mock(Negotiation.class); + String payload = "Test Payload"; + + InformationSubmission infoSub = + new InformationSubmission(requirement, resource, negotiation, payload); + assertNull(infoSub.getId()); + assertEquals(requirement, infoSub.getRequirement()); + assertEquals(resource, infoSub.getResource()); + assertEquals(negotiation, infoSub.getNegotiation()); + assertEquals(payload, infoSub.getPayload()); + } + + @Test + public void testSettersAndGetters() { + InformationRequirement requirement = Mockito.mock(InformationRequirement.class); + Resource resource = Mockito.mock(Resource.class); + Negotiation negotiation = Mockito.mock(Negotiation.class); + String payload = "Test Payload"; + + InformationSubmission infoSub = + new InformationSubmission(requirement, resource, negotiation, payload); + Long id = 1L; + + infoSub.setId(id); + infoSub.setRequirement(requirement); + infoSub.setResource(resource); + infoSub.setNegotiation(negotiation); + infoSub.setPayload(payload); + + assertEquals(id, infoSub.getId()); + assertEquals(requirement, infoSub.getRequirement()); + assertEquals(resource, infoSub.getResource()); + assertEquals(negotiation, infoSub.getNegotiation()); + assertEquals(payload, infoSub.getPayload()); + } } From f6b422eb3aeed63cdcc4b7f56cdda3a363872302 Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Thu, 1 Aug 2024 14:59:50 +0200 Subject: [PATCH 12/24] feat(info-submission): add state machine interaction Signed-off-by: RadovanTomik --- .../service/ResourceLifecycleServiceImpl.java | 9 +++- .../NegotiationLifecycleServiceImplTest.java | 41 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/bbmri_eric/negotiator/service/ResourceLifecycleServiceImpl.java b/src/main/java/eu/bbmri_eric/negotiator/service/ResourceLifecycleServiceImpl.java index 4915cc439..c51aee02e 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/service/ResourceLifecycleServiceImpl.java +++ b/src/main/java/eu/bbmri_eric/negotiator/service/ResourceLifecycleServiceImpl.java @@ -4,6 +4,7 @@ import eu.bbmri_eric.negotiator.configuration.state_machine.resource.NegotiationResourceEvent; import eu.bbmri_eric.negotiator.configuration.state_machine.resource.NegotiationResourceState; import eu.bbmri_eric.negotiator.database.repository.InformationRequirementRepository; +import eu.bbmri_eric.negotiator.database.repository.InformationSubmissionRepository; import eu.bbmri_eric.negotiator.database.repository.NegotiationRepository; import eu.bbmri_eric.negotiator.exceptions.EntityNotFoundException; import eu.bbmri_eric.negotiator.exceptions.WrongRequestException; @@ -32,6 +33,7 @@ public class ResourceLifecycleServiceImpl implements ResourceLifecycleService { private final NegotiationRepository negotiationRepository; private final InformationRequirementRepository requirementRepository; + private final InformationSubmissionRepository requirementSubmissionRepository; private final PersistStateMachineHandler persistStateMachineHandler; @@ -42,11 +44,13 @@ public class ResourceLifecycleServiceImpl implements ResourceLifecycleService { public ResourceLifecycleServiceImpl( NegotiationRepository negotiationRepository, InformationRequirementRepository requirementRepository, + InformationSubmissionRepository requirementSubmissionRepository, @Qualifier("resourcePersistHandler") PersistStateMachineHandler persistStateMachineHandler, @Qualifier("resourceStateMachine") StateMachine stateMachine, PersonService personService) { this.negotiationRepository = negotiationRepository; this.requirementRepository = requirementRepository; + this.requirementSubmissionRepository = requirementSubmissionRepository; this.persistStateMachineHandler = persistStateMachineHandler; this.stateMachine = stateMachine; this.personService = personService; @@ -97,7 +101,10 @@ private void traverseState( public NegotiationResourceState sendEvent( String negotiationId, String resourceId, NegotiationResourceEvent negotiationResourceEvent) throws WrongRequestException, EntityNotFoundException { - if (requirementRepository.existsByForEvent(negotiationResourceEvent)) { + if (requirementRepository.existsByForEvent(negotiationResourceEvent) + && !requirementSubmissionRepository.existsByResource_SourceIdAndNegotiation_Id( + resourceId, negotiationId)) { + log.warn("Req not met"); throw new StateMachineException( "The requirement for this operation was not met. Please make sure you have submitted the required form and try again."); } diff --git a/src/test/java/eu/bbmri_eric/negotiator/integration/service/NegotiationLifecycleServiceImplTest.java b/src/test/java/eu/bbmri_eric/negotiator/integration/service/NegotiationLifecycleServiceImplTest.java index cf3972bc5..f37cd942a 100644 --- a/src/test/java/eu/bbmri_eric/negotiator/integration/service/NegotiationLifecycleServiceImplTest.java +++ b/src/test/java/eu/bbmri_eric/negotiator/integration/service/NegotiationLifecycleServiceImplTest.java @@ -12,15 +12,19 @@ import eu.bbmri_eric.negotiator.configuration.state_machine.resource.NegotiationResourceState; import eu.bbmri_eric.negotiator.database.model.AccessForm; import eu.bbmri_eric.negotiator.database.model.InformationRequirement; +import eu.bbmri_eric.negotiator.database.model.InformationSubmission; import eu.bbmri_eric.negotiator.database.model.Negotiation; import eu.bbmri_eric.negotiator.database.model.NegotiationLifecycleRecord; import eu.bbmri_eric.negotiator.database.model.NegotiationResourceLifecycleRecord; import eu.bbmri_eric.negotiator.database.model.Person; import eu.bbmri_eric.negotiator.database.model.Request; +import eu.bbmri_eric.negotiator.database.model.Resource; import eu.bbmri_eric.negotiator.database.repository.AccessFormRepository; import eu.bbmri_eric.negotiator.database.repository.InformationRequirementRepository; +import eu.bbmri_eric.negotiator.database.repository.InformationSubmissionRepository; import eu.bbmri_eric.negotiator.database.repository.NegotiationRepository; import eu.bbmri_eric.negotiator.database.repository.RequestRepository; +import eu.bbmri_eric.negotiator.database.repository.ResourceRepository; import eu.bbmri_eric.negotiator.dto.negotiation.NegotiationCreateDTO; import eu.bbmri_eric.negotiator.dto.negotiation.NegotiationDTO; import eu.bbmri_eric.negotiator.exceptions.EntityNotFoundException; @@ -60,6 +64,8 @@ public class NegotiationLifecycleServiceImplTest { @Autowired RequestRepository requestRepository; @Autowired InformationRequirementRepository requirementRepository; @Autowired AccessFormRepository accessFormRepository; + @Autowired private InformationSubmissionRepository informationSubmissionRepository; + @Autowired private ResourceRepository resourceRepository; private void checkNegotiationResourceRecordPresenceWithAssignedState( String negotiationId, NegotiationResourceState negotiationResourceState) { @@ -370,6 +376,41 @@ void sendEventForResource_notFulfilledRequirement_throwsStateMachineException() NegotiationResourceEvent.CONTACT)); } + @Test + @WithMockNegotiatorUser(authorities = "ROLE_ADMIN", id = 109L) + @Transactional + void sendEventForResource_fulfilledRequirement_ok() throws IOException { + NegotiationDTO negotiationDTO = saveNegotiation(); + negotiationLifecycleService.sendEvent(negotiationDTO.getId(), NegotiationEvent.APPROVE); + assertEquals( + NegotiationResourceState.REPRESENTATIVE_CONTACTED, + NegotiationResourceState.valueOf( + negotiationService + .findById(negotiationDTO.getId(), false) + .getStatusForResource("biobank:1:collection:2"))); + AccessForm accessForm = accessFormRepository.findAll().stream().findFirst().get(); + InformationRequirement requirement = + requirementRepository.save( + new InformationRequirement( + accessForm, NegotiationResourceEvent.MARK_AS_CHECKING_AVAILABILITY)); + Negotiation negotiation = negotiationRepository.findById(negotiationDTO.getId()).get(); + Resource resource = resourceRepository.findBySourceId("biobank:1:collection:2").get(); + informationSubmissionRepository.saveAndFlush( + new InformationSubmission(requirement, resource, negotiation, "{}")); + assertTrue( + requirementRepository.existsByForEvent( + NegotiationResourceEvent.MARK_AS_CHECKING_AVAILABILITY)); + assertTrue( + informationSubmissionRepository.existsByResource_SourceIdAndNegotiation_Id( + resource.getSourceId(), negotiation.getId())); + assertEquals( + NegotiationResourceState.CHECKING_AVAILABILITY, + resourceLifecycleService.sendEvent( + negotiation.getId(), + "biobank:1:collection:2", + NegotiationResourceEvent.MARK_AS_CHECKING_AVAILABILITY)); + } + @Test void getPossibleEventsForResource_nonApprovedNegotiation_throwsEntityNotFound() throws IOException { From fb68608d54dde147d48fda4355d95206b9ae9d55 Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Thu, 1 Aug 2024 15:22:40 +0200 Subject: [PATCH 13/24] feat(info-submission): add checks for duplicate submissions Signed-off-by: RadovanTomik --- .../InformationSubmissionRepository.java | 3 ++ .../InformationSubmissionServiceImpl.java | 6 +++ .../InformationRequirementControllerTest.java | 40 +++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/src/main/java/eu/bbmri_eric/negotiator/database/repository/InformationSubmissionRepository.java b/src/main/java/eu/bbmri_eric/negotiator/database/repository/InformationSubmissionRepository.java index 5ceb8a051..d6c6db4f2 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/database/repository/InformationSubmissionRepository.java +++ b/src/main/java/eu/bbmri_eric/negotiator/database/repository/InformationSubmissionRepository.java @@ -8,5 +8,8 @@ public interface InformationSubmissionRepository extends JpaRepository { boolean existsByResource_SourceIdAndNegotiation_Id(String sourceId, String negotiationId); + boolean existsByResource_SourceIdAndNegotiation_IdAndRequirement_Id( + String sourceId, String negotiationId, Long requirementId); + Set findAllByNegotiation_Id(String negotiationId); } diff --git a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java index 452df0b30..e0d24f994 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java +++ b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java @@ -16,6 +16,7 @@ import eu.bbmri_eric.negotiator.events.InformationSubmissionEvent; import eu.bbmri_eric.negotiator.exceptions.EntityNotFoundException; import eu.bbmri_eric.negotiator.exceptions.ForbiddenRequestException; +import eu.bbmri_eric.negotiator.exceptions.WrongRequestException; import jakarta.transaction.Transactional; import java.util.List; import java.util.Optional; @@ -67,6 +68,11 @@ public SubmittedInformationDTO submit( } InformationSubmission submission = buildSubmissionEntity(informationSubmissionDTO, informationRequirementId, negotiationId); + if (informationSubmissionRepository.existsByResource_SourceIdAndNegotiation_IdAndRequirement_Id( + submission.getResource().getSourceId(), negotiationId, informationRequirementId)) { + throw new WrongRequestException( + "The required information for this resource was already provided"); + } submission = informationSubmissionRepository.saveAndFlush(submission); applicationEventPublisher.publishEvent(new InformationSubmissionEvent(this, negotiationId)); return modelMapper.map(submission, SubmittedInformationDTO.class); diff --git a/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java b/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java index 364134e04..4d197502d 100644 --- a/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java +++ b/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java @@ -394,6 +394,46 @@ void submit_notARepresentative_403() throws Exception { .andExpect(status().isForbidden()); } + @Test + @WithUserDetails("TheBiobanker") + @Transactional + void submit_2forTheSameRequirement_400() throws Exception { + Negotiation negotiation = negotiationRepository.findAll().iterator().next(); + InformationRequirementDTO informationRequirementDTO = + informationRequirementServiceImpl.createInformationRequirement( + new InformationRequirementCreateDTO(1L, NegotiationResourceEvent.CONTACT)); + String payload = + """ + { + "sample-type": "DNA", + "num-of-subjects": 10, + "num-of-samples": 20, + "volume-per-sample": 5 + } + """; + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonPayload = mapper.readTree(payload); + InformationSubmissionDTO submissionDTO = + new InformationSubmissionDTO( + negotiation.getResources().iterator().next().getId(), jsonPayload); + mockMvc + .perform( + MockMvcRequestBuilders.post( + INFO_SUBMISSION_ENDPOINT.formatted( + negotiation.getId(), informationRequirementDTO.getId())) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(submissionDTO))) + .andExpect(status().isOk()); + mockMvc + .perform( + MockMvcRequestBuilders.post( + INFO_SUBMISSION_ENDPOINT.formatted( + negotiation.getId(), informationRequirementDTO.getId())) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(submissionDTO))) + .andExpect(status().isBadRequest()); + } + private long createInformationSubmission() throws Exception { Negotiation negotiation = negotiationRepository.findAll().iterator().next(); InformationRequirementDTO informationRequirementDTO = From 980072356cd911abfafd0b883c4562cec6d81a68 Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Thu, 1 Aug 2024 15:39:08 +0200 Subject: [PATCH 14/24] chore(info-submission): update documentation Signed-off-by: RadovanTomik --- .../api/controller/v3/InformationSubmissionController.java | 1 + .../negotiator/database/model/InformationSubmission.java | 1 + 2 files changed, 2 insertions(+) diff --git a/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationSubmissionController.java b/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationSubmissionController.java index 85d469f7c..347e632be 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationSubmissionController.java +++ b/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationSubmissionController.java @@ -50,6 +50,7 @@ public String getSummaryInformation( NegotiationDTO negotiationDTO = negotiationService.findById(negotiationId, false); InformationRequirementDTO requirementDTO = requirementService.getInformationRequirement(requirementId); + // TODO Finish the implementation return "{}"; } diff --git a/src/main/java/eu/bbmri_eric/negotiator/database/model/InformationSubmission.java b/src/main/java/eu/bbmri_eric/negotiator/database/model/InformationSubmission.java index 92e6196b2..b255d4531 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/database/model/InformationSubmission.java +++ b/src/main/java/eu/bbmri_eric/negotiator/database/model/InformationSubmission.java @@ -11,6 +11,7 @@ import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; +/** Represents a submission of additional information by the resource representative. */ @Setter @Getter @Entity From 74f604cea783ce245c6f117d8dbea0a334b01f32 Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Mon, 5 Aug 2024 11:13:57 +0200 Subject: [PATCH 15/24] feat(resources): add operation links Signed-off-by: RadovanTomik --- .../repository/ResourceRepository.java | 19 ++++++++-- .../dto/SubmittedInformationDTO.java | 1 + .../dto/resource/ResourceWithStatusDTO.java | 2 +- .../mappers/NegotiationModelMapper.java | 2 +- .../mappers/ResourceWithStatusAssembler.java | 38 +++++++++++++++---- 5 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/main/java/eu/bbmri_eric/negotiator/database/repository/ResourceRepository.java b/src/main/java/eu/bbmri_eric/negotiator/database/repository/ResourceRepository.java index cb2f51af9..dd4052ef0 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/database/repository/ResourceRepository.java +++ b/src/main/java/eu/bbmri_eric/negotiator/database/repository/ResourceRepository.java @@ -17,10 +17,21 @@ public interface ResourceRepository extends JpaRepository { @Query( value = - "SELECT rs.id as id, rspn.negotiation_id as negotiationId, rs.name as name, rs.source_id as sourceId, rspn.current_state as currentState, o.name as organizationName, o.external_id as organizationExternalId, o.id as organizationId " - + "FROM resource rs join resource_state_per_negotiation rspn on rs.source_id = rspn.resource_id " - + "join organization o on o.id = rs.organization_id " - + "where rspn.negotiation_id = :negotiationId", + """ +select rs.id as id, + r.negotiation_id as negotiationId, + rs.name as name, + rs.source_id as sourceId, + rspn.current_state as currentState, + o.name as organizationName, + o.external_id as organizationExternalId, + o.id as organizationId +from resource rs + join public.request_resources_link rrl on rs.id = rrl.resource_id + join public.organization o on o.id = rs.organization_id + left join public.resource_state_per_negotiation rspn on rs.source_id = rspn.resource_id + join public.request r on r.id = rrl.request_id; +""", nativeQuery = true) List findByNegotiation(String negotiationId); diff --git a/src/main/java/eu/bbmri_eric/negotiator/dto/SubmittedInformationDTO.java b/src/main/java/eu/bbmri_eric/negotiator/dto/SubmittedInformationDTO.java index 7631fa2b2..16e761d6f 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/dto/SubmittedInformationDTO.java +++ b/src/main/java/eu/bbmri_eric/negotiator/dto/SubmittedInformationDTO.java @@ -16,5 +16,6 @@ public class SubmittedInformationDTO { @NotNull private Long id; @NotNull private Long resourceId; + private Long requirementId; @NotNull private JsonNode payload; } diff --git a/src/main/java/eu/bbmri_eric/negotiator/dto/resource/ResourceWithStatusDTO.java b/src/main/java/eu/bbmri_eric/negotiator/dto/resource/ResourceWithStatusDTO.java index 310f29877..860fb7710 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/dto/resource/ResourceWithStatusDTO.java +++ b/src/main/java/eu/bbmri_eric/negotiator/dto/resource/ResourceWithStatusDTO.java @@ -32,5 +32,5 @@ public class ResourceWithStatusDTO { @Nullable private OrganizationDTO organization; - private NegotiationResourceState status; + private NegotiationResourceState currentState; } diff --git a/src/main/java/eu/bbmri_eric/negotiator/mappers/NegotiationModelMapper.java b/src/main/java/eu/bbmri_eric/negotiator/mappers/NegotiationModelMapper.java index b5f9e10d6..f427d421c 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/mappers/NegotiationModelMapper.java +++ b/src/main/java/eu/bbmri_eric/negotiator/mappers/NegotiationModelMapper.java @@ -98,7 +98,7 @@ private ResourceWithStatusDTO buildResourceWithStatus( .organization(modelMapper.map(resource.getOrganization(), OrganizationDTO.class)); NegotiationResourceState state = statePerResource.get(resource.getSourceId()); if (state != null) { - builder.status(state); + builder.currentState(state); } return builder.build(); } diff --git a/src/main/java/eu/bbmri_eric/negotiator/mappers/ResourceWithStatusAssembler.java b/src/main/java/eu/bbmri_eric/negotiator/mappers/ResourceWithStatusAssembler.java index 25a03e2c5..c70ae53aa 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/mappers/ResourceWithStatusAssembler.java +++ b/src/main/java/eu/bbmri_eric/negotiator/mappers/ResourceWithStatusAssembler.java @@ -4,13 +4,16 @@ import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; import eu.bbmri_eric.negotiator.api.controller.v3.InformationRequirementController; +import eu.bbmri_eric.negotiator.api.controller.v3.InformationSubmissionController; import eu.bbmri_eric.negotiator.api.controller.v3.NegotiationController; import eu.bbmri_eric.negotiator.api.controller.v3.ResourceController; import eu.bbmri_eric.negotiator.configuration.state_machine.resource.NegotiationResourceEvent; import eu.bbmri_eric.negotiator.configuration.state_machine.resource.NegotiationResourceState; import eu.bbmri_eric.negotiator.dto.InformationRequirementDTO; +import eu.bbmri_eric.negotiator.dto.SubmittedInformationDTO; import eu.bbmri_eric.negotiator.dto.resource.ResourceWithStatusDTO; import eu.bbmri_eric.negotiator.service.InformationRequirementService; +import eu.bbmri_eric.negotiator.service.InformationSubmissionService; import eu.bbmri_eric.negotiator.service.ResourceLifecycleService; import java.util.ArrayList; import java.util.HashMap; @@ -34,21 +37,25 @@ public class ResourceWithStatusAssembler ResourceWithStatusDTO, EntityModel> { private final ResourceLifecycleService resourceLifecycleService; private final InformationRequirementService informationRequirementService; - private final Map> cache = - new HashMap<>(); - private final Map> requirementsCache = + private final InformationSubmissionService informationSubmissionService; + private Map> cache = new HashMap<>(); + private Map> requirementsCache = new HashMap<>(); + private List submittedInformationCache = new ArrayList<>(); public ResourceWithStatusAssembler( ResourceLifecycleService resourceLifecycleService, - InformationRequirementService informationRequirementService) { + InformationRequirementService informationRequirementService, + InformationSubmissionService informationSubmissionService) { this.resourceLifecycleService = resourceLifecycleService; this.informationRequirementService = informationRequirementService; + this.informationSubmissionService = informationSubmissionService; } @Override public @NonNull EntityModel toModel( @NonNull ResourceWithStatusDTO entity) { + log.warn(entity.toString()); List links = new ArrayList<>(); links.add( WebMvcLinkBuilder.linkTo(methodOn(ResourceController.class).getResourceById(entity.getId())) @@ -60,14 +67,29 @@ public ResourceWithStatusAssembler( private void attachLifeCycleLinks(@NonNull ResourceWithStatusDTO entity, List links) { try { + if (submittedInformationCache.isEmpty()) { + submittedInformationCache = + informationSubmissionService.findAllForNegotiation(entity.getNegotiationId()); + } + for (SubmittedInformationDTO submittedInformationDTO : submittedInformationCache) { + if (submittedInformationDTO.getResourceId().equals(entity.getId())) { + links.add( + linkTo( + methodOn(InformationSubmissionController.class) + .getInfoSubmission(submittedInformationDTO.getId())) + .withRel("submission-%s".formatted(submittedInformationDTO.getId())) + .withTitle("Submitted Information")); + } + } Set events; - if (cache.containsKey(entity.getStatus())) { - events = cache.get(entity.getStatus()).stream().collect(Collectors.toUnmodifiableSet()); + if (cache.containsKey(entity.getCurrentState())) { + events = + cache.get(entity.getCurrentState()).stream().collect(Collectors.toUnmodifiableSet()); } else { events = resourceLifecycleService.getPossibleEvents( entity.getNegotiationId(), entity.getSourceId()); - cache.put(entity.getStatus(), new ArrayList<>(events)); + cache.put(entity.getCurrentState(), new ArrayList<>(events)); } for (NegotiationResourceEvent event : events) { List requirements; @@ -81,7 +103,7 @@ private void attachLifeCycleLinks(@NonNull ResourceWithStatusDTO entity, List
  • Date: Mon, 5 Aug 2024 12:30:38 +0200 Subject: [PATCH 16/24] feat(resources): add operation links Signed-off-by: RadovanTomik --- .../mappers/ResourceWithStatusAssembler.java | 104 ++++++++-------- .../api/v3/NegotiationControllerTests.java | 1 + .../api/v3/ResourceControllerTests.java | 117 ++++++++++++++++++ 3 files changed, 170 insertions(+), 52 deletions(-) diff --git a/src/main/java/eu/bbmri_eric/negotiator/mappers/ResourceWithStatusAssembler.java b/src/main/java/eu/bbmri_eric/negotiator/mappers/ResourceWithStatusAssembler.java index c70ae53aa..8c41230f8 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/mappers/ResourceWithStatusAssembler.java +++ b/src/main/java/eu/bbmri_eric/negotiator/mappers/ResourceWithStatusAssembler.java @@ -8,7 +8,6 @@ import eu.bbmri_eric.negotiator.api.controller.v3.NegotiationController; import eu.bbmri_eric.negotiator.api.controller.v3.ResourceController; import eu.bbmri_eric.negotiator.configuration.state_machine.resource.NegotiationResourceEvent; -import eu.bbmri_eric.negotiator.configuration.state_machine.resource.NegotiationResourceState; import eu.bbmri_eric.negotiator.dto.InformationRequirementDTO; import eu.bbmri_eric.negotiator.dto.SubmittedInformationDTO; import eu.bbmri_eric.negotiator.dto.resource.ResourceWithStatusDTO; @@ -16,11 +15,8 @@ import eu.bbmri_eric.negotiator.service.InformationSubmissionService; import eu.bbmri_eric.negotiator.service.ResourceLifecycleService; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import lombok.NonNull; import lombok.extern.apachecommons.CommonsLog; import org.springframework.hateoas.CollectionModel; @@ -38,9 +34,7 @@ public class ResourceWithStatusAssembler private final ResourceLifecycleService resourceLifecycleService; private final InformationRequirementService informationRequirementService; private final InformationSubmissionService informationSubmissionService; - private Map> cache = new HashMap<>(); - private Map> requirementsCache = - new HashMap<>(); + private static List requirementsCache = new ArrayList<>(); private List submittedInformationCache = new ArrayList<>(); public ResourceWithStatusAssembler( @@ -61,74 +55,80 @@ public ResourceWithStatusAssembler( WebMvcLinkBuilder.linkTo(methodOn(ResourceController.class).getResourceById(entity.getId())) .withSelfRel()); links.add(linkTo(ResourceController.class).withRel("resources")); - attachLifeCycleLinks(entity, links); - return EntityModel.of(entity).add(links); - } - - private void attachLifeCycleLinks(@NonNull ResourceWithStatusDTO entity, List links) { + addLifecycleLink(entity, links); try { if (submittedInformationCache.isEmpty()) { submittedInformationCache = informationSubmissionService.findAllForNegotiation(entity.getNegotiationId()); } - for (SubmittedInformationDTO submittedInformationDTO : submittedInformationCache) { - if (submittedInformationDTO.getResourceId().equals(entity.getId())) { + for (SubmittedInformationDTO info : submittedInformationCache) { + if (info.getResourceId().equals(entity.getId())) { links.add( linkTo( methodOn(InformationSubmissionController.class) - .getInfoSubmission(submittedInformationDTO.getId())) - .withRel("submission-%s".formatted(submittedInformationDTO.getId())) - .withTitle("Submitted Information")); + .getInfoSubmission(info.getId())) + .withRel("submission-%s".formatted(info.getId())) + .withTitle("Submitted Information") + .withName("Submitted Information")); } } - Set events; - if (cache.containsKey(entity.getCurrentState())) { - events = - cache.get(entity.getCurrentState()).stream().collect(Collectors.toUnmodifiableSet()); - } else { - events = - resourceLifecycleService.getPossibleEvents( - entity.getNegotiationId(), entity.getSourceId()); - cache.put(entity.getCurrentState(), new ArrayList<>(events)); + } catch (Exception e) { + log.error("Could not attach submission links: " + e.getMessage()); + } + addRequirementLinks(links, entity.getId()); + return EntityModel.of(entity).add(links); + } + + private void addRequirementLinks(List links, Long resourceId) { + try { + if (requirementsCache.isEmpty()) { + requirementsCache = informationRequirementService.getAllInformationRequirements(); } - for (NegotiationResourceEvent event : events) { - List requirements; - if (requirementsCache.containsKey(event)) { - requirements = requirementsCache.get(event); - } else { - requirements = - informationRequirementService.getAllInformationRequirements().stream() - .filter( - informationRequirementDTO -> - informationRequirementDTO.getForResourceEvent().equals(event)) - .toList(); - requirementsCache.put(event, requirements); - log.warn(requirements); - } - for (InformationRequirementDTO dto : requirements) { + for (InformationRequirementDTO dto : requirementsCache) { + if (!submittedInformationCache.stream() + .anyMatch( + i -> + i.getResourceId().equals(resourceId) + && i.getRequirementId().equals(dto.getId()))) { links.add( linkTo( methodOn(InformationRequirementController.class) .findRequirementById(dto.getId())) .withRel("requirement-%s".formatted(dto.getId())) .withTitle("Requirement to fulfill") - .withName(event.toString() + " requirement")); + .withName(dto.getForResourceEvent().toString() + " requirement")); } - links.add( - linkTo( - methodOn(NegotiationController.class) - .sendEventForNegotiationResource( - entity.getNegotiationId(), entity.getSourceId(), event)) - .withRel(event.toString()) - .withTitle("Next Lifecycle event") - .withName(event.getLabel())); } } catch (Exception e) { - log.warn("Could not attach lifecycle links"); - log.warn(e.getMessage()); + log.error("Could not attach requirement links: " + e.getMessage()); } } + private void addLifecycleLink(@NonNull ResourceWithStatusDTO entity, List links) { + try { + Set events = + resourceLifecycleService.getPossibleEvents( + entity.getNegotiationId(), entity.getSourceId()); + for (NegotiationResourceEvent event : events) { + addLifecycleEventLink(entity, event, links); + } + } catch (Exception e) { + log.error("Could not attach lifecycle links: " + e.getMessage()); + } + } + + private static void addLifecycleEventLink( + @NonNull ResourceWithStatusDTO entity, NegotiationResourceEvent event, List links) { + links.add( + linkTo( + methodOn(NegotiationController.class) + .sendEventForNegotiationResource( + entity.getNegotiationId(), entity.getSourceId(), event)) + .withRel(event.toString()) + .withTitle("Next Lifecycle event") + .withName(event.getLabel())); + } + @Override public @NonNull CollectionModel> toCollectionModel( @NonNull Iterable entities) { diff --git a/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/NegotiationControllerTests.java b/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/NegotiationControllerTests.java index ce9706c22..fb7436b7f 100644 --- a/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/NegotiationControllerTests.java +++ b/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/NegotiationControllerTests.java @@ -1301,4 +1301,5 @@ void getPossibleLifecycleStages_noAuth_Ok() throws Exception { .perform(MockMvcRequestBuilders.get("%s/lifecycle".formatted(NEGOTIATIONS_URL))) .andExpect(status().isOk()); } + } diff --git a/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/ResourceControllerTests.java b/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/ResourceControllerTests.java index 96136cd1a..f69f1353c 100644 --- a/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/ResourceControllerTests.java +++ b/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/ResourceControllerTests.java @@ -6,16 +6,27 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import eu.bbmri_eric.negotiator.NegotiatorApplication; +import eu.bbmri_eric.negotiator.configuration.state_machine.resource.NegotiationResourceEvent; +import eu.bbmri_eric.negotiator.configuration.state_machine.resource.NegotiationResourceState; import eu.bbmri_eric.negotiator.database.model.DiscoveryService; +import eu.bbmri_eric.negotiator.database.model.InformationRequirement; +import eu.bbmri_eric.negotiator.database.model.InformationSubmission; import eu.bbmri_eric.negotiator.database.model.Negotiation; import eu.bbmri_eric.negotiator.database.model.Organization; import eu.bbmri_eric.negotiator.database.model.Resource; import eu.bbmri_eric.negotiator.database.repository.DiscoveryServiceRepository; +import eu.bbmri_eric.negotiator.database.repository.InformationRequirementRepository; +import eu.bbmri_eric.negotiator.database.repository.InformationSubmissionRepository; import eu.bbmri_eric.negotiator.database.repository.NegotiationRepository; import eu.bbmri_eric.negotiator.database.repository.OrganizationRepository; import eu.bbmri_eric.negotiator.database.repository.ResourceRepository; +import eu.bbmri_eric.negotiator.dto.InformationRequirementCreateDTO; +import eu.bbmri_eric.negotiator.service.InformationRequirementService; import eu.bbmri_eric.negotiator.unit.context.WithMockNegotiatorUser; +import jakarta.transaction.Transactional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -43,6 +54,9 @@ public class ResourceControllerTests { @Autowired private DiscoveryServiceRepository discoveryServiceRepository; private MockMvc mockMvc; @Autowired private NegotiationRepository negotiationRepository; + @Autowired private InformationRequirementService informationRequirementService; + @Autowired private InformationSubmissionRepository informationSubmissionRepository; + @Autowired private InformationRequirementRepository informationRequirementRepository; @BeforeEach public void before() { @@ -117,12 +131,14 @@ void getAllResourcesForNegotiation_isRepresentative_ok() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$._links").isNotEmpty()) .andExpect(jsonPath("$._embedded.resources[0].id").isNumber()) + .andExpect(jsonPath("$._embedded.resources[0].currentState").isString()) .andExpect(jsonPath("$._embedded.resources[0].sourceId").isString()) .andExpect(jsonPath("$._embedded.resources[0].organization.id").isNumber()) .andExpect(jsonPath("$._embedded.resources[0].organization.externalId").isString()) .andExpect(jsonPath("$._embedded.resources[0]._links").isMap()); } + @Test @WithMockNegotiatorUser(id = 102L) void getAllResourcesForNegotiation_notInvolved_403() throws Exception { @@ -133,4 +149,105 @@ void getAllResourcesForNegotiation_notInvolved_403() throws Exception { "/v3/negotiations/%s/resources".formatted(negotiation.getId()))) .andExpect(status().isForbidden()); } + + @Test + @WithMockNegotiatorUser(id = 109L, authorities = "ROLE_ADMIN") + @Transactional + void getAllResources_approvedNegotiation_resourceContainsLifecycleLinks() throws Exception { + Negotiation negotiation = negotiationRepository.findAll().stream().findFirst().get(); + negotiation.setStateForResource( + negotiation.getResources().iterator().next().getSourceId(), + NegotiationResourceState.REPRESENTATIVE_CONTACTED); + negotiationRepository.save(negotiation); + mockMvc + .perform( + MockMvcRequestBuilders.get( + "/v3/negotiations/%s/resources".formatted(negotiation.getId()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links").isNotEmpty()) + .andExpect(jsonPath("$._embedded.resources[0].id").isNumber()) + .andExpect(jsonPath("$._embedded.resources[0].currentState").isString()) + .andExpect(jsonPath("$._embedded.resources[0].sourceId").isString()) + .andExpect(jsonPath("$._embedded.resources[0].organization.id").isNumber()) + .andExpect(jsonPath("$._embedded.resources[0].organization.externalId").isString()) + .andExpect( + jsonPath("$._embedded.resources[0]._links.MARK_AS_CHECKING_AVAILABILITY").isNotEmpty()); + } + + @Test + @Transactional + @WithMockNegotiatorUser(authorities = "ROLE_ADMIN", id = 109L) + void getAllResources_approvedNegotiation_resourceContainsAllLinks() throws Exception { + Negotiation negotiation = negotiationRepository.findAll().stream().findFirst().get(); + Resource resource = negotiation.getResources().iterator().next(); + negotiation.setStateForResource( + resource.getSourceId(), NegotiationResourceState.REPRESENTATIVE_CONTACTED); + negotiationRepository.save(negotiation); + mockMvc + .perform( + MockMvcRequestBuilders.get( + "/v3/negotiations/%s/resources".formatted(negotiation.getId()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links").isNotEmpty()) + .andExpect(jsonPath("$._embedded.resources[0].id").isNumber()) + .andExpect(jsonPath("$._embedded.resources[0].currentState").isString()) + .andExpect(jsonPath("$._embedded.resources[0].sourceId").isString()) + .andExpect(jsonPath("$._embedded.resources[0].organization.id").isNumber()) + .andExpect(jsonPath("$._embedded.resources[0].organization.externalId").isString()) + .andExpect(jsonPath("$._embedded.resources[0]._links.requirement-1").doesNotExist()) + .andExpect( + jsonPath("$._embedded.resources[0]._links.MARK_AS_CHECKING_AVAILABILITY").isNotEmpty()); + Long requirementId = + informationRequirementService + .createInformationRequirement( + new InformationRequirementCreateDTO( + 1L, NegotiationResourceEvent.MARK_AS_CHECKING_AVAILABILITY)) + .getId(); + String payload = + """ + { + "sample-type": "DNA", + "num-of-subjects": 10, + "num-of-samples": 20, + "volume-per-sample": 5 + } + """; + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonPayload = mapper.readTree(payload); + mockMvc + .perform( + MockMvcRequestBuilders.get( + "/v3/negotiations/%s/resources".formatted(negotiation.getId()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links").isNotEmpty()) + .andExpect(jsonPath("$._embedded.resources[0].id").isNumber()) + .andExpect(jsonPath("$._embedded.resources[0].currentState").isString()) + .andExpect(jsonPath("$._embedded.resources[0].sourceId").isString()) + .andExpect(jsonPath("$._embedded.resources[0].organization.id").isNumber()) + .andExpect(jsonPath("$._embedded.resources[0].organization.externalId").isString()) + .andExpect(jsonPath("$._embedded.resources[0]._links.requirement-1").isNotEmpty()) + .andExpect( + jsonPath("$._embedded.resources[0]._links.MARK_AS_CHECKING_AVAILABILITY").isNotEmpty()); + + InformationRequirement informationRequirement = + informationRequirementRepository.findById(requirementId).get(); + informationSubmissionRepository.save( + new InformationSubmission( + informationRequirement, resource, negotiation, jsonPayload.toString())); + mockMvc + .perform( + MockMvcRequestBuilders.get( + "/v3/negotiations/%s/resources".formatted(negotiation.getId()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links").isNotEmpty()) + .andExpect(jsonPath("$._embedded.resources[0].id").isNumber()) + .andExpect(jsonPath("$._embedded.resources[0].currentState").isString()) + .andExpect(jsonPath("$._embedded.resources[0].sourceId").isString()) + .andExpect(jsonPath("$._embedded.resources[0].organization.id").isNumber()) + .andExpect(jsonPath("$._embedded.resources[0].organization.externalId").isString()) + .andExpect(jsonPath("$._embedded.resources[0]._links.submission-1").isNotEmpty()) + .andExpect(jsonPath("$._embedded.resources[0]._links.requirement-1").doesNotExist()) + .andExpect( + jsonPath("$._embedded.resources[0]._links.MARK_AS_CHECKING_AVAILABILITY").isNotEmpty()); + } } From 0eadefa85bdfa3104a4dbfb9d18d048f1a7493ed Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Mon, 5 Aug 2024 13:24:46 +0200 Subject: [PATCH 17/24] feat(resources): fix integration tests Signed-off-by: RadovanTomik --- .../service/NegotiationLifecycleServiceImplTest.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/test/java/eu/bbmri_eric/negotiator/integration/service/NegotiationLifecycleServiceImplTest.java b/src/test/java/eu/bbmri_eric/negotiator/integration/service/NegotiationLifecycleServiceImplTest.java index f37cd942a..4d6a6e7d7 100644 --- a/src/test/java/eu/bbmri_eric/negotiator/integration/service/NegotiationLifecycleServiceImplTest.java +++ b/src/test/java/eu/bbmri_eric/negotiator/integration/service/NegotiationLifecycleServiceImplTest.java @@ -383,11 +383,13 @@ void sendEventForResource_fulfilledRequirement_ok() throws IOException { NegotiationDTO negotiationDTO = saveNegotiation(); negotiationLifecycleService.sendEvent(negotiationDTO.getId(), NegotiationEvent.APPROVE); assertEquals( - NegotiationResourceState.REPRESENTATIVE_CONTACTED, - NegotiationResourceState.valueOf( - negotiationService - .findById(negotiationDTO.getId(), false) - .getStatusForResource("biobank:1:collection:2"))); + NegotiationResourceState.SUBMITTED, + resourceRepository.findByNegotiation(negotiationDTO.getId()).stream() + .filter( + resourceViewDTO -> resourceViewDTO.getSourceId().equals("biobank:1:collection:2")) + .findFirst() + .get() + .getCurrentState()); AccessForm accessForm = accessFormRepository.findAll().stream().findFirst().get(); InformationRequirement requirement = requirementRepository.save( From beb9b5801233ecfcf57e9a5cfbba64dd7147724c Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Mon, 5 Aug 2024 13:38:22 +0200 Subject: [PATCH 18/24] refactor: fix code style Signed-off-by: RadovanTomik --- .../mappers/ResourceWithStatusAssembler.java | 79 +++++++++++-------- .../api/v3/NegotiationControllerTests.java | 1 - .../api/v3/ResourceControllerTests.java | 1 - 3 files changed, 46 insertions(+), 35 deletions(-) diff --git a/src/main/java/eu/bbmri_eric/negotiator/mappers/ResourceWithStatusAssembler.java b/src/main/java/eu/bbmri_eric/negotiator/mappers/ResourceWithStatusAssembler.java index 8c41230f8..8108ea0b9 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/mappers/ResourceWithStatusAssembler.java +++ b/src/main/java/eu/bbmri_eric/negotiator/mappers/ResourceWithStatusAssembler.java @@ -49,34 +49,53 @@ public ResourceWithStatusAssembler( @Override public @NonNull EntityModel toModel( @NonNull ResourceWithStatusDTO entity) { - log.warn(entity.toString()); + List links = addWebLinks(entity); + return EntityModel.of(entity).add(links); + } + + private @NonNull List addWebLinks(@NonNull ResourceWithStatusDTO entity) { List links = new ArrayList<>(); links.add( WebMvcLinkBuilder.linkTo(methodOn(ResourceController.class).getResourceById(entity.getId())) .withSelfRel()); links.add(linkTo(ResourceController.class).withRel("resources")); addLifecycleLink(entity, links); + addSubmissionLinks(entity, links); + addRequirementLinks(links, entity.getId()); + return links; + } + + @Override + public @NonNull CollectionModel> toCollectionModel( + @NonNull Iterable entities) { + return RepresentationModelAssembler.super + .toCollectionModel(entities) + .add(WebMvcLinkBuilder.linkTo(ResourceController.class).withRel("resources")); + } + + private void addSubmissionLinks(@NonNull ResourceWithStatusDTO entity, List links) { try { if (submittedInformationCache.isEmpty()) { submittedInformationCache = informationSubmissionService.findAllForNegotiation(entity.getNegotiationId()); } for (SubmittedInformationDTO info : submittedInformationCache) { - if (info.getResourceId().equals(entity.getId())) { - links.add( - linkTo( - methodOn(InformationSubmissionController.class) - .getInfoSubmission(info.getId())) - .withRel("submission-%s".formatted(info.getId())) - .withTitle("Submitted Information") - .withName("Submitted Information")); - } + addSubmissionLink(entity, links, info); } } catch (Exception e) { log.error("Could not attach submission links: " + e.getMessage()); } - addRequirementLinks(links, entity.getId()); - return EntityModel.of(entity).add(links); + } + + private static void addSubmissionLink( + @NonNull ResourceWithStatusDTO entity, List links, SubmittedInformationDTO info) { + if (info.getResourceId().equals(entity.getId())) { + links.add( + linkTo(methodOn(InformationSubmissionController.class).getInfoSubmission(info.getId())) + .withRel("submission-%s".formatted(info.getId())) + .withTitle("Submitted Information") + .withName("Submitted Information")); + } } private void addRequirementLinks(List links, Long resourceId) { @@ -85,25 +104,27 @@ private void addRequirementLinks(List links, Long resourceId) { requirementsCache = informationRequirementService.getAllInformationRequirements(); } for (InformationRequirementDTO dto : requirementsCache) { - if (!submittedInformationCache.stream() - .anyMatch( - i -> - i.getResourceId().equals(resourceId) - && i.getRequirementId().equals(dto.getId()))) { - links.add( - linkTo( - methodOn(InformationRequirementController.class) - .findRequirementById(dto.getId())) - .withRel("requirement-%s".formatted(dto.getId())) - .withTitle("Requirement to fulfill") - .withName(dto.getForResourceEvent().toString() + " requirement")); - } + addRequirementLink(links, resourceId, dto); } } catch (Exception e) { log.error("Could not attach requirement links: " + e.getMessage()); } } + private void addRequirementLink( + List links, Long resourceId, InformationRequirementDTO dto) { + if (submittedInformationCache.stream() + .noneMatch( + i -> + i.getResourceId().equals(resourceId) && i.getRequirementId().equals(dto.getId()))) { + links.add( + linkTo(methodOn(InformationRequirementController.class).findRequirementById(dto.getId())) + .withRel("requirement-%s".formatted(dto.getId())) + .withTitle("Requirement to fulfill") + .withName(dto.getForResourceEvent().toString() + " requirement")); + } + } + private void addLifecycleLink(@NonNull ResourceWithStatusDTO entity, List links) { try { Set events = @@ -128,12 +149,4 @@ private static void addLifecycleEventLink( .withTitle("Next Lifecycle event") .withName(event.getLabel())); } - - @Override - public @NonNull CollectionModel> toCollectionModel( - @NonNull Iterable entities) { - return RepresentationModelAssembler.super - .toCollectionModel(entities) - .add(WebMvcLinkBuilder.linkTo(ResourceController.class).withRel("resources")); - } } diff --git a/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/NegotiationControllerTests.java b/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/NegotiationControllerTests.java index fb7436b7f..ce9706c22 100644 --- a/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/NegotiationControllerTests.java +++ b/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/NegotiationControllerTests.java @@ -1301,5 +1301,4 @@ void getPossibleLifecycleStages_noAuth_Ok() throws Exception { .perform(MockMvcRequestBuilders.get("%s/lifecycle".formatted(NEGOTIATIONS_URL))) .andExpect(status().isOk()); } - } diff --git a/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/ResourceControllerTests.java b/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/ResourceControllerTests.java index f69f1353c..0bdab4f4a 100644 --- a/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/ResourceControllerTests.java +++ b/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/ResourceControllerTests.java @@ -138,7 +138,6 @@ void getAllResourcesForNegotiation_isRepresentative_ok() throws Exception { .andExpect(jsonPath("$._embedded.resources[0]._links").isMap()); } - @Test @WithMockNegotiatorUser(id = 102L) void getAllResourcesForNegotiation_notInvolved_403() throws Exception { From c6ae73182c3a5ac196a3fa52f2aed53972a6541d Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Mon, 5 Aug 2024 16:17:39 +0200 Subject: [PATCH 19/24] fix(resource): fix find all for negotiation query Signed-off-by: RadovanTomik --- .../repository/ResourceRepository.java | 5 +-- .../mappers/ResourceWithStatusAssembler.java | 31 ++++++++++++------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/main/java/eu/bbmri_eric/negotiator/database/repository/ResourceRepository.java b/src/main/java/eu/bbmri_eric/negotiator/database/repository/ResourceRepository.java index dd4052ef0..4ebd8533e 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/database/repository/ResourceRepository.java +++ b/src/main/java/eu/bbmri_eric/negotiator/database/repository/ResourceRepository.java @@ -29,8 +29,9 @@ public interface ResourceRepository extends JpaRepository { from resource rs join public.request_resources_link rrl on rs.id = rrl.resource_id join public.organization o on o.id = rs.organization_id - left join public.resource_state_per_negotiation rspn on rs.source_id = rspn.resource_id - join public.request r on r.id = rrl.request_id; + join public.request r on r.id = rrl.request_id + left join public.resource_state_per_negotiation rspn on rs.source_id = rspn.resource_id and r.negotiation_id = rspn.negotiation_id + where r.negotiation_id = :negotiationId; """, nativeQuery = true) List findByNegotiation(String negotiationId); diff --git a/src/main/java/eu/bbmri_eric/negotiator/mappers/ResourceWithStatusAssembler.java b/src/main/java/eu/bbmri_eric/negotiator/mappers/ResourceWithStatusAssembler.java index 8108ea0b9..84c8e9b85 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/mappers/ResourceWithStatusAssembler.java +++ b/src/main/java/eu/bbmri_eric/negotiator/mappers/ResourceWithStatusAssembler.java @@ -49,6 +49,9 @@ public ResourceWithStatusAssembler( @Override public @NonNull EntityModel toModel( @NonNull ResourceWithStatusDTO entity) { + requirementsCache = informationRequirementService.getAllInformationRequirements(); + submittedInformationCache = + informationSubmissionService.findAllForNegotiation(entity.getNegotiationId()); List links = addWebLinks(entity); return EntityModel.of(entity).add(links); } @@ -75,10 +78,6 @@ public ResourceWithStatusAssembler( private void addSubmissionLinks(@NonNull ResourceWithStatusDTO entity, List links) { try { - if (submittedInformationCache.isEmpty()) { - submittedInformationCache = - informationSubmissionService.findAllForNegotiation(entity.getNegotiationId()); - } for (SubmittedInformationDTO info : submittedInformationCache) { addSubmissionLink(entity, links, info); } @@ -90,19 +89,23 @@ private void addSubmissionLinks(@NonNull ResourceWithStatusDTO entity, List links, SubmittedInformationDTO info) { if (info.getResourceId().equals(entity.getId())) { + String name = + requirementsCache.stream() + .filter(dto -> dto.getId().equals(info.getRequirementId())) + .findFirst() + .get() + .getRequiredAccessForm() + .getName(); links.add( linkTo(methodOn(InformationSubmissionController.class).getInfoSubmission(info.getId())) .withRel("submission-%s".formatted(info.getId())) .withTitle("Submitted Information") - .withName("Submitted Information")); + .withName(name)); } } private void addRequirementLinks(List links, Long resourceId) { try { - if (requirementsCache.isEmpty()) { - requirementsCache = informationRequirementService.getAllInformationRequirements(); - } for (InformationRequirementDTO dto : requirementsCache) { addRequirementLink(links, resourceId, dto); } @@ -114,13 +117,17 @@ private void addRequirementLinks(List links, Long resourceId) { private void addRequirementLink( List links, Long resourceId, InformationRequirementDTO dto) { if (submittedInformationCache.stream() - .noneMatch( - i -> - i.getResourceId().equals(resourceId) && i.getRequirementId().equals(dto.getId()))) { + .noneMatch( + i -> + i.getResourceId().equals(resourceId) + && i.getRequirementId().equals(dto.getId())) + && links.stream() + .anyMatch( + link -> link.getRel().toString().equals(dto.getForResourceEvent().toString()))) { links.add( linkTo(methodOn(InformationRequirementController.class).findRequirementById(dto.getId())) .withRel("requirement-%s".formatted(dto.getId())) - .withTitle("Requirement to fulfill") + .withTitle(dto.getRequiredAccessForm().getName()) .withName(dto.getForResourceEvent().toString() + " requirement")); } } From 2b05ceb10f0175ea9cbc3ef40f38fcccd5283ccd Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Mon, 5 Aug 2024 16:38:40 +0200 Subject: [PATCH 20/24] fix(resource): fix find all for negotiation query Signed-off-by: RadovanTomik --- .../service/NegotiationLifecycleServiceImplTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/eu/bbmri_eric/negotiator/integration/service/NegotiationLifecycleServiceImplTest.java b/src/test/java/eu/bbmri_eric/negotiator/integration/service/NegotiationLifecycleServiceImplTest.java index 4d6a6e7d7..cc05964f3 100644 --- a/src/test/java/eu/bbmri_eric/negotiator/integration/service/NegotiationLifecycleServiceImplTest.java +++ b/src/test/java/eu/bbmri_eric/negotiator/integration/service/NegotiationLifecycleServiceImplTest.java @@ -383,7 +383,7 @@ void sendEventForResource_fulfilledRequirement_ok() throws IOException { NegotiationDTO negotiationDTO = saveNegotiation(); negotiationLifecycleService.sendEvent(negotiationDTO.getId(), NegotiationEvent.APPROVE); assertEquals( - NegotiationResourceState.SUBMITTED, + NegotiationResourceState.REPRESENTATIVE_CONTACTED, resourceRepository.findByNegotiation(negotiationDTO.getId()).stream() .filter( resourceViewDTO -> resourceViewDTO.getSourceId().equals("biobank:1:collection:2")) From 4dec533a5efcbc7743f7b48abb052616372ff8f1 Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Tue, 6 Aug 2024 10:47:34 +0200 Subject: [PATCH 21/24] refactor(InfoSubmission): refactor InformationSubmissionServiceImpl Signed-off-by: RadovanTomik --- .../InformationSubmissionServiceImpl.java | 69 +++++++++++-------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java index e0d24f994..d59f40050 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java +++ b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java @@ -61,13 +61,42 @@ public SubmittedInformationDTO submit( InformationSubmissionDTO informationSubmissionDTO, Long informationRequirementId, String negotiationId) { - if (!isAuthorizedToWrite( + verifyAuthorization(informationSubmissionDTO); + InformationSubmission submission = + buildSubmissionEntity(informationSubmissionDTO, informationRequirementId, negotiationId); + return saveInformationSubmission(informationRequirementId, negotiationId, submission); + } + + @Override + public SubmittedInformationDTO findById(Long id) { + InformationSubmission submission = + informationSubmissionRepository + .findById(id) + .orElseThrow(() -> new EntityNotFoundException(id)); + verifyReadAuthorization(submission); + return modelMapper.map(submission, SubmittedInformationDTO.class); + } + + private void verifyReadAuthorization(InformationSubmission submission) { + if (!isAuthorizedToRead( + submission.getNegotiation().getId(), NegotiatorUserDetailsService.getCurrentlyAuthenticatedUserInternalId(), - informationSubmissionDTO.getResourceId())) { + submission.getResource().getId())) { throw new ForbiddenRequestException("You are not authorized to perform this action"); } - InformationSubmission submission = - buildSubmissionEntity(informationSubmissionDTO, informationRequirementId, negotiationId); + } + + @Override + public List findAllForNegotiation(String negotiationId) { + return informationSubmissionRepository.findAllByNegotiation_Id(negotiationId).stream() + .map( + informationSubmission -> + modelMapper.map(informationSubmission, SubmittedInformationDTO.class)) + .toList(); + } + + private SubmittedInformationDTO saveInformationSubmission( + Long informationRequirementId, String negotiationId, InformationSubmission submission) { if (informationSubmissionRepository.existsByResource_SourceIdAndNegotiation_IdAndRequirement_Id( submission.getResource().getSourceId(), negotiationId, informationRequirementId)) { throw new WrongRequestException( @@ -78,6 +107,14 @@ public SubmittedInformationDTO submit( return modelMapper.map(submission, SubmittedInformationDTO.class); } + private void verifyAuthorization(InformationSubmissionDTO informationSubmissionDTO) { + if (!isAuthorizedToWrite( + NegotiatorUserDetailsService.getCurrentlyAuthenticatedUserInternalId(), + informationSubmissionDTO.getResourceId())) { + throw new ForbiddenRequestException("You are not authorized to perform this action"); + } + } + private boolean isAuthorizedToWrite(Long personId, Long resourceId) { Optional personOpt = personRepository.findById(personId); if (personOpt.isPresent()) { @@ -113,28 +150,4 @@ private boolean isAuthorizedToRead(String negotiationId, Long personId, Long res return new InformationSubmission( requirement, resource, negotiation, informationSubmissionDTO.getPayload().toString()); } - - @Override - public SubmittedInformationDTO findById(Long id) { - InformationSubmission submission = - informationSubmissionRepository - .findById(id) - .orElseThrow(() -> new EntityNotFoundException(id)); - if (!isAuthorizedToRead( - submission.getNegotiation().getId(), - NegotiatorUserDetailsService.getCurrentlyAuthenticatedUserInternalId(), - submission.getResource().getId())) { - throw new ForbiddenRequestException("You are not authorized to perform this action"); - } - return modelMapper.map(submission, SubmittedInformationDTO.class); - } - - @Override - public List findAllForNegotiation(String negotiationId) { - return informationSubmissionRepository.findAllByNegotiation_Id(negotiationId).stream() - .map( - informationSubmission -> - modelMapper.map(informationSubmission, SubmittedInformationDTO.class)) - .toList(); - } } From cfd44c7eecef05db49991621cef35fe2a7b23940 Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Wed, 7 Aug 2024 11:05:19 +0200 Subject: [PATCH 22/24] feat(InfoSubmission): add generation and download of a summary file Signed-off-by: RadovanTomik --- pom.xml | 5 + .../v3/InformationSubmissionController.java | 35 +++-- .../api/controller/v3/NetworkController.java | 5 +- .../InformationSubmissionRepository.java | 4 + .../mappers/NegotiationModelAssembler.java | 23 ++- .../service/InformationSubmissionService.java | 9 ++ .../InformationSubmissionServiceImpl.java | 125 +++++++++++++++ .../InformationRequirementControllerTest.java | 148 +++++++++++------- .../NegotiationModelAssemblerTest.java | 2 +- 9 files changed, 278 insertions(+), 78 deletions(-) diff --git a/pom.xml b/pom.xml index d65a99715..29392d4e2 100644 --- a/pom.xml +++ b/pom.xml @@ -361,6 +361,11 @@ spring-boot-devtools 3.3.2 + + org.apache.commons + commons-csv + 1.8 + diff --git a/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationSubmissionController.java b/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationSubmissionController.java index 347e632be..4983da55f 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationSubmissionController.java +++ b/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationSubmissionController.java @@ -1,17 +1,18 @@ package eu.bbmri_eric.negotiator.api.controller.v3; -import eu.bbmri_eric.negotiator.dto.InformationRequirementDTO; import eu.bbmri_eric.negotiator.dto.InformationSubmissionDTO; import eu.bbmri_eric.negotiator.dto.SubmittedInformationDTO; -import eu.bbmri_eric.negotiator.dto.negotiation.NegotiationDTO; import eu.bbmri_eric.negotiator.service.InformationRequirementService; import eu.bbmri_eric.negotiator.service.InformationSubmissionService; import eu.bbmri_eric.negotiator.service.NegotiationService; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; +import java.io.IOException; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.MediaTypes; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -19,11 +20,10 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; @RestController -@RequestMapping( - value = InformationSubmissionController.BASE_URL, - produces = MediaTypes.HAL_JSON_VALUE) +@RequestMapping(value = InformationSubmissionController.BASE_URL) @Tag( name = "Submit required information", description = "Submit required information on behalf of a resource in a Negotiation.") @@ -44,18 +44,23 @@ public InformationSubmissionController( } @ResponseStatus(HttpStatus.OK) - @GetMapping("/negotiations/{negotiationId}/info-requirements/{requirementId}") - public String getSummaryInformation( - @PathVariable String negotiationId, @PathVariable Long requirementId) { - NegotiationDTO negotiationDTO = negotiationService.findById(negotiationId, false); - InformationRequirementDTO requirementDTO = - requirementService.getInformationRequirement(requirementId); - // TODO Finish the implementation - return "{}"; + @GetMapping( + value = "/negotiations/{negotiationId}/info-requirements/{requirementId}", + produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + public ResponseEntity getSummaryInformation( + @PathVariable String negotiationId, @PathVariable Long requirementId) throws IOException { + MultipartFile file = submissionService.createSummary(requirementId, negotiationId); + return ResponseEntity.ok() + .header("Content-Disposition", "attachment; filename=\"%s\"".formatted(file.getName())) + .contentType(MediaType.valueOf("text/csv")) + .body(file.getBytes()); } @ResponseStatus(HttpStatus.OK) - @PostMapping("/negotiations/{negotiationId}/info-requirements/{requirementId}") + @PostMapping( + value = "/negotiations/{negotiationId}/info-requirements/{requirementId}", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaTypes.HAL_JSON_VALUE) public EntityModel submitInformation( @PathVariable String negotiationId, @PathVariable Long requirementId, @@ -64,7 +69,7 @@ public EntityModel submitInformation( } @ResponseStatus(HttpStatus.OK) - @GetMapping("/info-submissions/{id}") + @GetMapping(value = "/info-submissions/{id}", produces = MediaTypes.HAL_JSON_VALUE) public EntityModel getInfoSubmission(@PathVariable Long id) { return EntityModel.of(submissionService.findById(id)); } diff --git a/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/NetworkController.java b/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/NetworkController.java index 8bafbcacb..c4a345a2b 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/NetworkController.java +++ b/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/NetworkController.java @@ -49,8 +49,7 @@ public class NetworkController { private final NegotiationService negotiationService; private final NetworkModelAssembler networkModelAssembler; private final ResourceModelAssembler resourceModelAssembler; - private final NegotiationModelAssembler negotiationModelAssembler = - new NegotiationModelAssembler(); + private final NegotiationModelAssembler negotiationModelAssembler; private final UserModelAssembler userModelAssembler; private final NetworkRepository networkRepository; @@ -61,6 +60,7 @@ public NetworkController( ResourceModelAssembler resourceModelAssembler, PersonService personService, NegotiationService negotiationService, + NegotiationModelAssembler negotiationModelAssembler, UserModelAssembler userModelAssembler, NetworkRepository networkRepository) { this.networkService = networkService; @@ -69,6 +69,7 @@ public NetworkController( this.resourceModelAssembler = resourceModelAssembler; this.personService = personService; this.negotiationService = negotiationService; + this.negotiationModelAssembler = negotiationModelAssembler; this.userModelAssembler = userModelAssembler; this.networkRepository = networkRepository; } diff --git a/src/main/java/eu/bbmri_eric/negotiator/database/repository/InformationSubmissionRepository.java b/src/main/java/eu/bbmri_eric/negotiator/database/repository/InformationSubmissionRepository.java index d6c6db4f2..5ebf74ce8 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/database/repository/InformationSubmissionRepository.java +++ b/src/main/java/eu/bbmri_eric/negotiator/database/repository/InformationSubmissionRepository.java @@ -1,6 +1,7 @@ package eu.bbmri_eric.negotiator.database.repository; import eu.bbmri_eric.negotiator.database.model.InformationSubmission; +import java.util.List; import java.util.Set; import org.springframework.data.jpa.repository.JpaRepository; @@ -12,4 +13,7 @@ boolean existsByResource_SourceIdAndNegotiation_IdAndRequirement_Id( String sourceId, String negotiationId, Long requirementId); Set findAllByNegotiation_Id(String negotiationId); + + List findAllByRequirement_IdAndNegotiation_Id( + Long requirementId, String negotiationId); } diff --git a/src/main/java/eu/bbmri_eric/negotiator/mappers/NegotiationModelAssembler.java b/src/main/java/eu/bbmri_eric/negotiator/mappers/NegotiationModelAssembler.java index b25e1a2ae..b308ef342 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/mappers/NegotiationModelAssembler.java +++ b/src/main/java/eu/bbmri_eric/negotiator/mappers/NegotiationModelAssembler.java @@ -4,11 +4,15 @@ import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; import eu.bbmri_eric.negotiator.api.controller.v3.AttachmentController; +import eu.bbmri_eric.negotiator.api.controller.v3.InformationSubmissionController; import eu.bbmri_eric.negotiator.api.controller.v3.NegotiationController; import eu.bbmri_eric.negotiator.api.controller.v3.NetworkController; import eu.bbmri_eric.negotiator.api.controller.v3.utils.NegotiationSortField; +import eu.bbmri_eric.negotiator.dto.InformationRequirementDTO; import eu.bbmri_eric.negotiator.dto.negotiation.NegotiationDTO; import eu.bbmri_eric.negotiator.dto.negotiation.NegotiationFilters; +import eu.bbmri_eric.negotiator.service.InformationRequirementService; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -26,11 +30,28 @@ @Component public class NegotiationModelAssembler implements RepresentationModelAssembler> { - public NegotiationModelAssembler() {} + private final InformationRequirementService requirementService; + + public NegotiationModelAssembler(InformationRequirementService requirementService) { + this.requirementService = requirementService; + } @Override public @NonNull EntityModel toModel(@NonNull NegotiationDTO entity) { List links = new ArrayList<>(); + for (InformationRequirementDTO requirement : + requirementService.getAllInformationRequirements()) { + try { + links.add( + WebMvcLinkBuilder.linkTo( + methodOn(InformationSubmissionController.class) + .getSummaryInformation(entity.getId(), requirement.getId())) + .withRel("Requirement summary") + .withTitle(requirement.getRequiredAccessForm().getName() + " summary")); + } catch (IOException e) { + throw new RuntimeException(e); + } + } links.add( WebMvcLinkBuilder.linkTo(methodOn(NegotiationController.class).retrieve(entity.getId())) .withSelfRel()); diff --git a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionService.java b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionService.java index 58c7137b8..a0da591d6 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionService.java +++ b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionService.java @@ -2,7 +2,9 @@ import eu.bbmri_eric.negotiator.dto.InformationSubmissionDTO; import eu.bbmri_eric.negotiator.dto.SubmittedInformationDTO; +import java.io.IOException; import java.util.List; +import org.springframework.web.multipart.MultipartFile; public interface InformationSubmissionService { /** @@ -33,4 +35,11 @@ SubmittedInformationDTO submit( * @return a list of all linked submissions */ List findAllForNegotiation(String negotiationId); + + /** + * Generate a summary of all submissions for a given requirement. + * + * @return a summary file + */ + MultipartFile createSummary(Long requirementId, String negotiationId) throws IOException; } diff --git a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java index d59f40050..e4f1a12aa 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java +++ b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java @@ -1,5 +1,8 @@ package eu.bbmri_eric.negotiator.service; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import eu.bbmri_eric.negotiator.configuration.security.auth.NegotiatorUserDetailsService; import eu.bbmri_eric.negotiator.database.model.InformationRequirement; import eu.bbmri_eric.negotiator.database.model.InformationSubmission; @@ -18,13 +21,25 @@ import eu.bbmri_eric.negotiator.exceptions.ForbiddenRequestException; import eu.bbmri_eric.negotiator.exceptions.WrongRequestException; import jakarta.transaction.Transactional; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; import lombok.NonNull; import lombok.extern.apachecommons.CommonsLog; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; import org.modelmapper.ModelMapper; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; @Service @Transactional @@ -95,6 +110,25 @@ public List findAllForNegotiation(String negotiationId) .toList(); } + @Override + public MultipartFile createSummary(Long requirementId, String negotiationId) throws IOException { + if (negotiationRepository.existsByIdAndCreatedBy_Id( + negotiationId, NegotiatorUserDetailsService.getCurrentlyAuthenticatedUserInternalId()) + || NegotiatorUserDetailsService.isCurrentlyAuthenticatedUserAdmin()) { + List allSubmissions = + informationSubmissionRepository.findAllByRequirement_IdAndNegotiation_Id( + requirementId, negotiationId); + String name = + informationRequirementRepository + .findById(requirementId) + .orElseThrow(() -> new EntityNotFoundException(requirementId)) + .getRequiredAccessForm() + .getName(); + return generateCSVFile(allSubmissions, "%s-summary.csv".formatted(name)); + } + throw new ForbiddenRequestException("You are not authorized to perform this action"); + } + private SubmittedInformationDTO saveInformationSubmission( Long informationRequirementId, String negotiationId, InformationSubmission submission) { if (informationSubmissionRepository.existsByResource_SourceIdAndNegotiation_IdAndRequirement_Id( @@ -107,6 +141,97 @@ private SubmittedInformationDTO saveInformationSubmission( return modelMapper.map(submission, SubmittedInformationDTO.class); } + private MultipartFile generateCSVFile(List submissions, String fileName) + throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + Set jsonKeys = generatedHeadersFromResponses(submissions, objectMapper); + List headers = setHeaders(jsonKeys); + if (submissions.isEmpty()) { + headers = new ArrayList<>(); + } + ByteArrayOutputStream byteArrayOutputStream = + createCSVAsByteArray(submissions, headers, objectMapper, jsonKeys); + return new MockMultipartFile( + fileName, fileName, "text/csv", byteArrayOutputStream.toByteArray()); + } + + private static @NonNull List setHeaders(Set jsonKeys) { + List headers = new ArrayList<>(); + headers.add("resourceId"); + headers.addAll(jsonKeys); + return headers; + } + + private static @NonNull ByteArrayOutputStream createCSVAsByteArray( + List submissions, + List headers, + ObjectMapper objectMapper, + Set jsonKeys) { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + if (submissions.isEmpty()) return byteArrayOutputStream; + try (CSVPrinter printer = + new CSVPrinter( + new OutputStreamWriter(byteArrayOutputStream), + CSVFormat.DEFAULT.withHeader(headers.toArray(new String[0])))) { + for (InformationSubmission submission : submissions) { + buildRow(objectMapper, jsonKeys, submission, printer); + } + } catch (Exception e) { + log.error("Could not generate CSV file", e); + throw new InternalError("Could not generate the CSV file. Please try again later"); + } + return byteArrayOutputStream; + } + + private static void buildRow( + ObjectMapper objectMapper, + Set jsonKeys, + InformationSubmission submission, + CSVPrinter printer) + throws IOException { + List row = new ArrayList<>(); + row.add(submission.getResource().getSourceId()); + JsonNode payload = objectMapper.readTree(submission.getPayload()); + Map flattenedPayload = flattenJson(payload); + for (String key : jsonKeys) { + String value = flattenedPayload.getOrDefault(key, ""); + row.add(value); + } + printer.printRecord(row); + } + + private static @NonNull Set generatedHeadersFromResponses( + List submissions, ObjectMapper objectMapper) + throws JsonProcessingException { + Set jsonKeys = new TreeSet<>(); + for (InformationSubmission submission : submissions) { + JsonNode payload = objectMapper.readTree(submission.getPayload()); + Map flattenedPayload = flattenJson(payload); + jsonKeys.addAll(flattenedPayload.keySet()); + } + return jsonKeys; + } + + private static Map flattenJson(JsonNode node) { + Map flattenedMap = new LinkedHashMap<>(); + flattenJsonHelper("", node, flattenedMap); + return flattenedMap; + } + + private static void flattenJsonHelper( + String prefix, JsonNode node, Map flattenedMap) { + if (node.isObject()) { + node.fieldNames() + .forEachRemaining( + field -> { + String newPrefix = prefix.isEmpty() ? field : prefix + "." + field; + flattenJsonHelper(newPrefix, node.get(field), flattenedMap); + }); + } else if (node.isValueNode()) { + flattenedMap.put(prefix, node.asText()); + } + } + private void verifyAuthorization(InformationSubmissionDTO informationSubmissionDTO) { if (!isAuthorizedToWrite( NegotiatorUserDetailsService.getCurrentlyAuthenticatedUserInternalId(), diff --git a/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java b/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java index 4d197502d..f677934f3 100644 --- a/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java +++ b/src/test/java/eu/bbmri_eric/negotiator/integration/api/v3/InformationRequirementControllerTest.java @@ -3,6 +3,7 @@ import static org.hamcrest.core.Is.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -20,12 +21,14 @@ import eu.bbmri_eric.negotiator.dto.InformationRequirementDTO; import eu.bbmri_eric.negotiator.dto.InformationSubmissionDTO; import eu.bbmri_eric.negotiator.service.InformationRequirementServiceImpl; +import eu.bbmri_eric.negotiator.unit.context.WithMockNegotiatorUser; import jakarta.transaction.Transactional; import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.security.test.context.support.WithUserDetails; @@ -238,38 +241,6 @@ void findRequirementById_nonExistingId_notFound() throws Exception { .andExpect(status().isNotFound()); } - @Test - void getSummary_nonExistingId_notFound() throws Exception { - mockMvc - .perform(MockMvcRequestBuilders.get(INFO_SUBMISSION_ENDPOINT.formatted("1", "1"))) - .andExpect(status().isNotFound()); - } - - @Test - void getSummary_nonExistingRequirement_notFound() throws Exception { - Negotiation negotiation = negotiationRepository.findAll().iterator().next(); - mockMvc - .perform( - MockMvcRequestBuilders.get( - INFO_SUBMISSION_ENDPOINT.formatted(negotiation.getId(), "1"))) - .andExpect(status().isNotFound()); - } - - @Test - void getSummary_noSubmissions_emptyResponse() throws Exception { - Negotiation negotiation = negotiationRepository.findAll().iterator().next(); - InformationRequirementDTO informationRequirementDTO = - informationRequirementServiceImpl.createInformationRequirement( - new InformationRequirementCreateDTO(1L, NegotiationResourceEvent.CONTACT)); - mockMvc - .perform( - MockMvcRequestBuilders.get( - INFO_SUBMISSION_ENDPOINT.formatted( - negotiation.getId(), informationRequirementDTO.getId()))) - .andExpect(status().isOk()) - .andExpect(content().json("{}")); - } - @Test @WithUserDetails("TheBiobanker") @Transactional @@ -434,43 +405,102 @@ void submit_2forTheSameRequirement_400() throws Exception { .andExpect(status().isBadRequest()); } - private long createInformationSubmission() throws Exception { + @Test + @WithMockNegotiatorUser(id = 109L) + void generateSummary_representative_403() throws Exception { + Negotiation negotiation = negotiationRepository.findAll().iterator().next(); + mockMvc + .perform( + MockMvcRequestBuilders.get( + INFO_SUBMISSION_ENDPOINT.formatted(negotiation.getId(), 9999L))) + .andExpect(status().isForbidden()); + } + + @Test + @WithMockNegotiatorUser(id = 109L, authorities = "ROLE_ADMIN") + void generateSummary_nonExistentRequirement_404() throws Exception { + Negotiation negotiation = negotiationRepository.findAll().iterator().next(); + mockMvc + .perform( + MockMvcRequestBuilders.get( + INFO_SUBMISSION_ENDPOINT.formatted(negotiation.getId(), 9999L))) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockNegotiatorUser(id = 109L, authorities = "ROLE_ADMIN") + void generateSummary_noSubmissions_404() throws Exception { + Negotiation negotiation = negotiationRepository.findAll().iterator().next(); + InformationRequirementDTO requirementDTO = + informationRequirementServiceImpl.createInformationRequirement( + new InformationRequirementCreateDTO(1L, NegotiationResourceEvent.CONTACT)); + mockMvc + .perform( + MockMvcRequestBuilders.get( + INFO_SUBMISSION_ENDPOINT.formatted(negotiation.getId(), requirementDTO.getId()))) + .andExpect(status().isOk()) + .andExpect( + header() + .string( + HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=\"%s-summary.csv\"" + .formatted(requirementDTO.getRequiredAccessForm().getName()))) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, "text/csv")) + .andExpect(content().string("")); + } + + @Test + @WithMockNegotiatorUser(id = 109L, authorities = "ROLE_ADMIN") + @Transactional + void generateSummary_1submission_ok() throws Exception { Negotiation negotiation = negotiationRepository.findAll().iterator().next(); InformationRequirementDTO informationRequirementDTO = informationRequirementServiceImpl.createInformationRequirement( new InformationRequirementCreateDTO(1L, NegotiationResourceEvent.CONTACT)); String payload = """ - { - "sample-type": "DNA", - "num-of-subjects": 10, - "num-of-samples": 20, - "volume-per-sample": 5 - } - """; + { + "sample-type": "DNA", + "num-of-subjects": 10, + "num-of-samples": 20, + "volume-per-sample": 5 + } + """; ObjectMapper mapper = new ObjectMapper(); JsonNode jsonPayload = mapper.readTree(payload); InformationSubmissionDTO submissionDTO = new InformationSubmissionDTO( negotiation.getResources().iterator().next().getId(), jsonPayload); - MvcResult mvcResult = - mockMvc - .perform( - MockMvcRequestBuilders.post( - INFO_SUBMISSION_ENDPOINT.formatted( - negotiation.getId(), informationRequirementDTO.getId())) - .contentType(MediaType.APPLICATION_JSON) - .content(new ObjectMapper().writeValueAsString(submissionDTO))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.id").isNumber()) - .andExpect(jsonPath("$.resourceId").value(submissionDTO.getResourceId())) - .andExpect(jsonPath("$.payload.sample-type").value("DNA")) - .andReturn(); - long id = - new ObjectMapper() - .readTree(mvcResult.getResponse().getContentAsString()) - .get("id") - .asLong(); - return id; + mockMvc + .perform( + MockMvcRequestBuilders.post( + INFO_SUBMISSION_ENDPOINT.formatted( + negotiation.getId(), informationRequirementDTO.getId())) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(submissionDTO))) + .andExpect(status().isOk()); + String expectedResponse = + """ +resourceId,num-of-samples,num-of-subjects,sample-type,volume-per-sample +biobank:1:collection:1,20,10,DNA,5 +"""; + mockMvc + .perform( + MockMvcRequestBuilders.get( + INFO_SUBMISSION_ENDPOINT.formatted( + negotiation.getId(), informationRequirementDTO.getId()))) + .andExpect(status().isOk()) + .andExpect( + header() + .string( + HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=\"%s-summary.csv\"" + .formatted(informationRequirementDTO.getRequiredAccessForm().getName()))) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, "text/csv")) + .andExpect(content().string(normalizeLineEndingsToCRLF(expectedResponse))); + } + + private String normalizeLineEndingsToCRLF(String text) { + return text.replace("\r\n", "\n").replace("\n", "\r\n"); } } diff --git a/src/test/java/eu/bbmri_eric/negotiator/unit/mappers/NegotiationModelAssemblerTest.java b/src/test/java/eu/bbmri_eric/negotiator/unit/mappers/NegotiationModelAssemblerTest.java index 438dcb79c..f2d829748 100644 --- a/src/test/java/eu/bbmri_eric/negotiator/unit/mappers/NegotiationModelAssemblerTest.java +++ b/src/test/java/eu/bbmri_eric/negotiator/unit/mappers/NegotiationModelAssemblerTest.java @@ -13,7 +13,7 @@ import org.springframework.data.domain.PageImpl; public class NegotiationModelAssemblerTest { - NegotiationModelAssembler negotiationModelAssembler = new NegotiationModelAssembler(); + NegotiationModelAssembler negotiationModelAssembler = new NegotiationModelAssembler(null); @Test void toModel_null_nullPointer() { From 2e9dca1dd6b1c78db7f8ef6147102850155f5274 Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Wed, 7 Aug 2024 11:48:09 +0200 Subject: [PATCH 23/24] feat(InfoSubmission): add generation and download of a summary file Signed-off-by: RadovanTomik --- .../v3/InformationSubmissionController.java | 15 ++++++---- .../controller/v3/NegotiationController.java | 4 +++ .../mappers/NegotiationModelAssembler.java | 29 ++++++++++--------- .../service/InformationSubmissionService.java | 3 +- .../InformationSubmissionServiceImpl.java | 16 ++++++---- 5 files changed, 40 insertions(+), 27 deletions(-) diff --git a/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationSubmissionController.java b/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationSubmissionController.java index 4983da55f..d024b4b53 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationSubmissionController.java +++ b/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/InformationSubmissionController.java @@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ServerErrorException; @RestController @RequestMapping(value = InformationSubmissionController.BASE_URL) @@ -48,12 +49,16 @@ public InformationSubmissionController( value = "/negotiations/{negotiationId}/info-requirements/{requirementId}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) public ResponseEntity getSummaryInformation( - @PathVariable String negotiationId, @PathVariable Long requirementId) throws IOException { + @PathVariable String negotiationId, @PathVariable Long requirementId) { MultipartFile file = submissionService.createSummary(requirementId, negotiationId); - return ResponseEntity.ok() - .header("Content-Disposition", "attachment; filename=\"%s\"".formatted(file.getName())) - .contentType(MediaType.valueOf("text/csv")) - .body(file.getBytes()); + try { + return ResponseEntity.ok() + .header("Content-Disposition", "attachment; filename=\"%s\"".formatted(file.getName())) + .contentType(MediaType.valueOf("text/csv")) + .body(file.getBytes()); + } catch (IOException e) { + throw new ServerErrorException("Failed to create summary information", e); + } } @ResponseStatus(HttpStatus.OK) diff --git a/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/NegotiationController.java b/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/NegotiationController.java index fc9b5b03e..b17c1e48c 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/NegotiationController.java +++ b/src/main/java/eu/bbmri_eric/negotiator/api/controller/v3/NegotiationController.java @@ -241,6 +241,10 @@ private static void checkAuthorization(Long id) { public EntityModel retrieve(@Valid @PathVariable String id) { NegotiationDTO negotiationDTO = negotiationService.findById(id, true); if (isAuthorizedForNegotiation(negotiationDTO)) { + if (negotiationService.isNegotiationCreator(id) + || NegotiatorUserDetailsService.isCurrentlyAuthenticatedUserAdmin()) { + return assembler.toModelWithRequirementLink(negotiationDTO); + } return assembler.toModel(negotiationDTO); } else { throw new ResponseStatusException(HttpStatus.FORBIDDEN); diff --git a/src/main/java/eu/bbmri_eric/negotiator/mappers/NegotiationModelAssembler.java b/src/main/java/eu/bbmri_eric/negotiator/mappers/NegotiationModelAssembler.java index b308ef342..835fda4e0 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/mappers/NegotiationModelAssembler.java +++ b/src/main/java/eu/bbmri_eric/negotiator/mappers/NegotiationModelAssembler.java @@ -12,7 +12,6 @@ import eu.bbmri_eric.negotiator.dto.negotiation.NegotiationDTO; import eu.bbmri_eric.negotiator.dto.negotiation.NegotiationFilters; import eu.bbmri_eric.negotiator.service.InformationRequirementService; -import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -36,22 +35,24 @@ public NegotiationModelAssembler(InformationRequirementService requirementServic this.requirementService = requirementService; } - @Override - public @NonNull EntityModel toModel(@NonNull NegotiationDTO entity) { - List links = new ArrayList<>(); + public @NonNull EntityModel toModelWithRequirementLink( + @NonNull NegotiationDTO entity) { + EntityModel entityModel = toModel(entity); for (InformationRequirementDTO requirement : requirementService.getAllInformationRequirements()) { - try { - links.add( - WebMvcLinkBuilder.linkTo( - methodOn(InformationSubmissionController.class) - .getSummaryInformation(entity.getId(), requirement.getId())) - .withRel("Requirement summary") - .withTitle(requirement.getRequiredAccessForm().getName() + " summary")); - } catch (IOException e) { - throw new RuntimeException(e); - } + entityModel.add( + WebMvcLinkBuilder.linkTo( + methodOn(InformationSubmissionController.class) + .getSummaryInformation(entity.getId(), requirement.getId())) + .withRel("Requirement summary %s".formatted(requirement.getId())) + .withTitle(requirement.getRequiredAccessForm().getName() + " summary")); } + return entityModel; + } + + @Override + public @NonNull EntityModel toModel(@NonNull NegotiationDTO entity) { + List links = new ArrayList<>(); links.add( WebMvcLinkBuilder.linkTo(methodOn(NegotiationController.class).retrieve(entity.getId())) .withSelfRel()); diff --git a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionService.java b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionService.java index a0da591d6..8332bab98 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionService.java +++ b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionService.java @@ -2,7 +2,6 @@ import eu.bbmri_eric.negotiator.dto.InformationSubmissionDTO; import eu.bbmri_eric.negotiator.dto.SubmittedInformationDTO; -import java.io.IOException; import java.util.List; import org.springframework.web.multipart.MultipartFile; @@ -41,5 +40,5 @@ SubmittedInformationDTO submit( * * @return a summary file */ - MultipartFile createSummary(Long requirementId, String negotiationId) throws IOException; + MultipartFile createSummary(Long requirementId, String negotiationId); } diff --git a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java index e4f1a12aa..cda84106f 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java +++ b/src/main/java/eu/bbmri_eric/negotiator/service/InformationSubmissionServiceImpl.java @@ -111,7 +111,7 @@ public List findAllForNegotiation(String negotiationId) } @Override - public MultipartFile createSummary(Long requirementId, String negotiationId) throws IOException { + public MultipartFile createSummary(Long requirementId, String negotiationId) { if (negotiationRepository.existsByIdAndCreatedBy_Id( negotiationId, NegotiatorUserDetailsService.getCurrentlyAuthenticatedUserInternalId()) || NegotiatorUserDetailsService.isCurrentlyAuthenticatedUserAdmin()) { @@ -141,8 +141,7 @@ private SubmittedInformationDTO saveInformationSubmission( return modelMapper.map(submission, SubmittedInformationDTO.class); } - private MultipartFile generateCSVFile(List submissions, String fileName) - throws IOException { + private MultipartFile generateCSVFile(List submissions, String fileName) { ObjectMapper objectMapper = new ObjectMapper(); Set jsonKeys = generatedHeadersFromResponses(submissions, objectMapper); List headers = setHeaders(jsonKeys); @@ -201,11 +200,16 @@ private static void buildRow( } private static @NonNull Set generatedHeadersFromResponses( - List submissions, ObjectMapper objectMapper) - throws JsonProcessingException { + List submissions, ObjectMapper objectMapper) { Set jsonKeys = new TreeSet<>(); for (InformationSubmission submission : submissions) { - JsonNode payload = objectMapper.readTree(submission.getPayload()); + JsonNode payload = null; + try { + payload = objectMapper.readTree(submission.getPayload()); + } catch (JsonProcessingException e) { + log.error("Could not generate JSON payload", e); + throw new RuntimeException(e); + } Map flattenedPayload = flattenJson(payload); jsonKeys.addAll(flattenedPayload.keySet()); } From 31031cb703f56ffe5ef94e9fd596bbb28df8d89b Mon Sep 17 00:00:00 2001 From: RadovanTomik Date: Fri, 9 Aug 2024 15:47:04 +0200 Subject: [PATCH 24/24] fix: remove unnecessary logging Signed-off-by: RadovanTomik --- .../negotiator/service/ResourceLifecycleServiceImpl.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/eu/bbmri_eric/negotiator/service/ResourceLifecycleServiceImpl.java b/src/main/java/eu/bbmri_eric/negotiator/service/ResourceLifecycleServiceImpl.java index c51aee02e..3f84dc8f7 100644 --- a/src/main/java/eu/bbmri_eric/negotiator/service/ResourceLifecycleServiceImpl.java +++ b/src/main/java/eu/bbmri_eric/negotiator/service/ResourceLifecycleServiceImpl.java @@ -104,7 +104,6 @@ public NegotiationResourceState sendEvent( if (requirementRepository.existsByForEvent(negotiationResourceEvent) && !requirementSubmissionRepository.existsByResource_SourceIdAndNegotiation_Id( resourceId, negotiationId)) { - log.warn("Req not met"); throw new StateMachineException( "The requirement for this operation was not met. Please make sure you have submitted the required form and try again."); }