Skip to content

Commit

Permalink
Merge pull request #5 from DiSSCo/feature/error-recovery-controller
Browse files Browse the repository at this point in the history
Add recovery endpoint
  • Loading branch information
southeo authored Jun 18, 2024
2 parents 6006e78 + 6d45370 commit aebbbf3
Show file tree
Hide file tree
Showing 19 changed files with 744 additions and 23 deletions.
12 changes: 12 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package eu.dissco.core.datacitepublisher.configuration;

import eu.dissco.core.datacitepublisher.properties.DataCiteConnectionProperties;
import eu.dissco.core.datacitepublisher.properties.HandleConnectionProperties;
import eu.dissco.core.datacitepublisher.web.WebClientUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
Expand All @@ -13,17 +14,29 @@
@RequiredArgsConstructor
public class WebClientConfig {

private final DataCiteConnectionProperties properties;
@Bean
public WebClient webClient() {
private final DataCiteConnectionProperties dataciteProperties;
private final HandleConnectionProperties handleProperties;

@Bean("datacite")
public WebClient dataciteClient() {
ExchangeFilterFunction errorResponseFilter = ExchangeFilterFunction
.ofResponseProcessor(WebClientUtils::exchangeFilterResponseProcessor);
return WebClient.builder()
.baseUrl(properties.getEndpoint())
.baseUrl(dataciteProperties.getEndpoint())
.filter(errorResponseFilter)
.defaultHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.api+json")
.defaultHeaders(
header -> header.setBasicAuth(properties.getRepositoryId(), properties.getPassword()))
header -> header.setBasicAuth(dataciteProperties.getRepositoryId(), dataciteProperties.getPassword()))
.build();
}

@Bean("handle")
public WebClient handleClient() {
ExchangeFilterFunction errorResponseFilter = ExchangeFilterFunction
.ofResponseProcessor(WebClientUtils::exchangeFilterResponseProcessor);
return WebClient.builder()
.baseUrl(handleProperties.getEndpoint())
.filter(errorResponseFilter)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package eu.dissco.core.datacitepublisher.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import eu.dissco.core.datacitepublisher.domain.RecoveryEvent;
import eu.dissco.core.datacitepublisher.exceptions.DataCiteApiException;
import eu.dissco.core.datacitepublisher.exceptions.HandleResolutionException;
import eu.dissco.core.datacitepublisher.service.RecoveryService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
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.RestController;

@RestController
@RequestMapping("/api/v1/datacite-recovery")
@RequiredArgsConstructor
@Slf4j
public class RecoveryController {

private final RecoveryService recoveryService;

@PostMapping("")
public ResponseEntity<Void> recoverPids(@RequestBody RecoveryEvent event)
throws HandleResolutionException, DataCiteApiException, JsonProcessingException {
recoveryService.recoverDataciteDois(event);
return ResponseEntity.ok(null);
}


}
30 changes: 30 additions & 0 deletions src/main/java/eu/dissco/core/datacitepublisher/domain/FdoType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package eu.dissco.core.datacitepublisher.domain;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public enum FdoType {
@JsonProperty("https://hdl.handle.net/21.T11148/894b1e6cad57e921764e") DIGITAL_SPECIMEN,
@JsonProperty("https://hdl.handle.net/21.T11148/bbad8c4e101e8af01115") MEDIA_OBJECT;

public static FdoType fromString(String type) {
if ("https://hdl.handle.net/21.T11148/894b1e6cad57e921764e".equals(type)) {
return DIGITAL_SPECIMEN;
}
if ("https://hdl.handle.net/21.T11148/bbad8c4e101e8af01115".equals(type)) {
return MEDIA_OBJECT;
}
log.error("Invalid DOI type: {}", type);
throw new IllegalStateException();
}

@Override
public String toString(){
if (this.equals(DIGITAL_SPECIMEN)) return "https://hdl.handle.net/21.T11148/894b1e6cad57e921764e";
return "https://hdl.handle.net/21.T11148/bbad8c4e101e8af01115";
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package eu.dissco.core.datacitepublisher.domain;

import java.util.List;

public record RecoveryEvent(
List<String> handles,
EventType eventType
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package eu.dissco.core.datacitepublisher.exceptions;

public class HandleResolutionException extends Exception {

public HandleResolutionException(){
super();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package eu.dissco.core.datacitepublisher.properties;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

@Data
@Validated
@ConfigurationProperties("handle")
public class HandleConnectionProperties {

@NotBlank
private String endpoint = "https://sandbox.dissco.tech/handle-manager/api/v1/pids/records";

@NotNull
private int maxHandles = 100;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package eu.dissco.core.datacitepublisher.security;

import jakarta.validation.constraints.NotNull;
import java.util.Collection;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.stereotype.Component;

@Component
public class JwtAuthConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();

@Override
public AbstractAuthenticationToken convert(@NotNull Jwt jwt) {
Collection<GrantedAuthority> authorities =
converterToStream(jwt).collect(Collectors.toSet());
return new JwtAuthenticationToken(jwt, authorities, getPrincipalClaimName(jwt));
}

private Stream<GrantedAuthority> converterToStream(Jwt jwt){
return Optional.of(jwtGrantedAuthoritiesConverter.convert(jwt)).stream()
.flatMap(Collection::stream);
}

private String getPrincipalClaimName(Jwt jwt) {
return jwt.getClaim(JwtClaimNames.SUB);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package eu.dissco.core.datacitepublisher.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;

@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {

@Bean
protected MethodSecurityExpressionHandler createExpressionHandler() {
return new DefaultMethodSecurityExpressionHandler();
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package eu.dissco.core.datacitepublisher.security;


import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

private final JwtAuthConverter jwtAuthConverter;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
.anyRequest()
.hasRole("orchestration-admin"));

http.oauth2ResourceServer(jwtoauth2ResourceServer -> jwtoauth2ResourceServer.jwt((
jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter)
)));

http.sessionManagement(sessionManagement -> sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package eu.dissco.core.datacitepublisher.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import eu.dissco.core.datacitepublisher.domain.DigitalSpecimenEvent;
import eu.dissco.core.datacitepublisher.domain.EventType;
import eu.dissco.core.datacitepublisher.domain.FdoType;
import eu.dissco.core.datacitepublisher.domain.MediaObjectEvent;
import eu.dissco.core.datacitepublisher.domain.RecoveryEvent;
import eu.dissco.core.datacitepublisher.exceptions.DataCiteApiException;
import eu.dissco.core.datacitepublisher.exceptions.HandleResolutionException;
import eu.dissco.core.datacitepublisher.properties.HandleConnectionProperties;
import eu.dissco.core.datacitepublisher.schemas.DigitalSpecimen;
import eu.dissco.core.datacitepublisher.schemas.MediaObject;
import eu.dissco.core.datacitepublisher.web.HandleClient;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class RecoveryService {

private final HandleClient handleClient;
private final DataCitePublisherService dataCitePublisherService;
private final ObjectMapper mapper;
private final HandleConnectionProperties handleConnectionProperties;

public void recoverDataciteDois(RecoveryEvent event)
throws HandleResolutionException, JsonProcessingException, DataCiteApiException {
int handlesProcessed = 0;
while (handlesProcessed < event.handles().size()){
int upperIndex = getUpperIndex(handlesProcessed, event.handles().size());
var handles = event.handles().subList(handlesProcessed, upperIndex);
processResolvedHandles(handles, event.eventType());
handlesProcessed = upperIndex;
}
}

private int getUpperIndex(int handlesProcessed, int totalHandles) {
if (handlesProcessed + handleConnectionProperties.getMaxHandles() > totalHandles) {
return totalHandles;
}
return handlesProcessed + handleConnectionProperties.getMaxHandles();
}

private void processResolvedHandles(List<String> handles, EventType eventType)
throws HandleResolutionException, DataCiteApiException, JsonProcessingException {

var handleResolutionResponse = handleClient.resolveHandles(handles);
if (handleResolutionResponse.get("data") != null && handleResolutionResponse.get("data").isArray()) {
var dataNodes = handleResolutionResponse.get("data");
for (var pidRecordJson : dataNodes) {
var type = FdoType.fromString(pidRecordJson.get("type").asText());
if (type.equals(FdoType.DIGITAL_SPECIMEN)) {
recoverDigitalSpecimen(pidRecordJson.get("attributes"), eventType);
} else {
recoverMediaObject(pidRecordJson.get("attributes"), eventType);
}
}
} else {
log.error("Unexpected response from handle api: {}", handleResolutionResponse);
throw new HandleResolutionException();
}
}



private void recoverDigitalSpecimen(JsonNode pidRecordAttributes, EventType eventType)
throws JsonProcessingException, DataCiteApiException {
var digitalSpecimen = mapper.treeToValue(pidRecordAttributes, DigitalSpecimen.class);
dataCitePublisherService.handleMessages(new DigitalSpecimenEvent(digitalSpecimen, eventType));
}

private void recoverMediaObject(JsonNode pidRecordAttributes, EventType eventType)
throws DataCiteApiException, JsonProcessingException {
var mediaObject = mapper.treeToValue(pidRecordAttributes, MediaObject.class);
dataCitePublisherService.handleMessages(new MediaObjectEvent(mediaObject, eventType));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.concurrent.ExecutionException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
Expand All @@ -16,6 +17,7 @@
@Component
@Slf4j
public class DataCiteClient {
@Qualifier(value = "datacite")
private final WebClient webClient;

public JsonNode sendDoiRequest(JsonNode requestBody, HttpMethod method, String doi) throws DataCiteApiException {
Expand Down
Loading

0 comments on commit aebbbf3

Please sign in to comment.