Skip to content

Commit

Permalink
Merge pull request #393 from BBMRI-ERIC/feat/submit_additional_inform…
Browse files Browse the repository at this point in the history
…ation

feat: add functionality to submit additional information
  • Loading branch information
RadovanTomik authored Aug 9, 2024
2 parents d3f6a03 + 31031cb commit 10c7510
Show file tree
Hide file tree
Showing 28 changed files with 1,346 additions and 70 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,11 @@
<artifactId>spring-boot-devtools</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.8</version>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package eu.bbmri_eric.negotiator.api.controller.v3;

import eu.bbmri_eric.negotiator.dto.InformationSubmissionDTO;
import eu.bbmri_eric.negotiator.dto.SubmittedInformationDTO;
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;
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;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ServerErrorException;

@RestController
@RequestMapping(value = InformationSubmissionController.BASE_URL)
@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;
private final InformationSubmissionService submissionService;
public static final String BASE_URL = "/v3";

public InformationSubmissionController(
InformationRequirementService requirementService,
NegotiationService negotiationService,
InformationSubmissionService submissionService) {
this.requirementService = requirementService;
this.negotiationService = negotiationService;
this.submissionService = submissionService;
}

@ResponseStatus(HttpStatus.OK)
@GetMapping(
value = "/negotiations/{negotiationId}/info-requirements/{requirementId}",
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<byte[]> getSummaryInformation(
@PathVariable String negotiationId, @PathVariable Long requirementId) {
MultipartFile file = submissionService.createSummary(requirementId, negotiationId);
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)
@PostMapping(
value = "/negotiations/{negotiationId}/info-requirements/{requirementId}",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaTypes.HAL_JSON_VALUE)
public EntityModel<SubmittedInformationDTO> submitInformation(
@PathVariable String negotiationId,
@PathVariable Long requirementId,
@RequestBody InformationSubmissionDTO dto) {
return EntityModel.of(submissionService.submit(dto, requirementId, negotiationId));
}

@ResponseStatus(HttpStatus.OK)
@GetMapping(value = "/info-submissions/{id}", produces = MediaTypes.HAL_JSON_VALUE)
public EntityModel<SubmittedInformationDTO> getInfoSubmission(@PathVariable Long id) {
return EntityModel.of(submissionService.findById(id));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ private static void checkAuthorization(Long id) {
public EntityModel<NegotiationDTO> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -61,6 +60,7 @@ public NetworkController(
ResourceModelAssembler resourceModelAssembler,
PersonService personService,
NegotiationService negotiationService,
NegotiationModelAssembler negotiationModelAssembler,
UserModelAssembler userModelAssembler,
NetworkRepository networkRepository) {
this.networkService = networkService;
Expand All @@ -69,6 +69,7 @@ public NetworkController(
this.resourceModelAssembler = resourceModelAssembler;
this.personService = personService;
this.negotiationService = negotiationService;
this.negotiationModelAssembler = negotiationModelAssembler;
this.userModelAssembler = userModelAssembler;
this.networkRepository = networkRepository;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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;

/** Represents a submission of additional information by the resource representative. */
@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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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;

public interface InformationSubmissionRepository
extends JpaRepository<InformationSubmission, Long> {
boolean existsByResource_SourceIdAndNegotiation_Id(String sourceId, String negotiationId);

boolean existsByResource_SourceIdAndNegotiation_IdAndRequirement_Id(
String sourceId, String negotiationId, Long requirementId);

Set<InformationSubmission> findAllByNegotiation_Id(String negotiationId);

List<InformationSubmission> findAllByRequirement_IdAndNegotiation_Id(
Long requirementId, String negotiationId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,22 @@ public interface ResourceRepository extends JpaRepository<Resource, Long> {

@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
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<ResourceViewDTO> findByNegotiation(String negotiationId);

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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;
private Long requirementId;
@NotNull private JsonNode payload;
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ public class ResourceWithStatusDTO {

@Nullable private OrganizationDTO organization;

private NegotiationResourceState status;
private NegotiationResourceState currentState;
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<InformationSubmission, SubmittedInformationDTO> typeMap =
modelMapper.createTypeMap(InformationSubmission.class, SubmittedInformationDTO.class);
Converter<String, JsonNode> 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);
}
}
Loading

0 comments on commit 10c7510

Please sign in to comment.