diff --git a/pom.xml b/pom.xml
index a535ed1..b6d30e1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -56,6 +56,18 @@
org.springframework.boot
spring-boot-starter-actuator
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-client
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
org.springframework.boot
diff --git a/src/main/java/eu/dissco/core/datacitepublisher/configuration/WebClientConfig.java b/src/main/java/eu/dissco/core/datacitepublisher/configuration/WebClientConfig.java
index 090f853..f11b7b8 100644
--- a/src/main/java/eu/dissco/core/datacitepublisher/configuration/WebClientConfig.java
+++ b/src/main/java/eu/dissco/core/datacitepublisher/configuration/WebClientConfig.java
@@ -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;
@@ -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();
}
diff --git a/src/main/java/eu/dissco/core/datacitepublisher/controller/RecoveryController.java b/src/main/java/eu/dissco/core/datacitepublisher/controller/RecoveryController.java
new file mode 100644
index 0000000..aae72a1
--- /dev/null
+++ b/src/main/java/eu/dissco/core/datacitepublisher/controller/RecoveryController.java
@@ -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 recoverPids(@RequestBody RecoveryEvent event)
+ throws HandleResolutionException, DataCiteApiException, JsonProcessingException {
+ recoveryService.recoverDataciteDois(event);
+ return ResponseEntity.ok(null);
+ }
+
+
+}
diff --git a/src/main/java/eu/dissco/core/datacitepublisher/domain/FdoType.java b/src/main/java/eu/dissco/core/datacitepublisher/domain/FdoType.java
new file mode 100644
index 0000000..1dd35de
--- /dev/null
+++ b/src/main/java/eu/dissco/core/datacitepublisher/domain/FdoType.java
@@ -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";
+ }
+
+
+}
diff --git a/src/main/java/eu/dissco/core/datacitepublisher/domain/RecoveryEvent.java b/src/main/java/eu/dissco/core/datacitepublisher/domain/RecoveryEvent.java
new file mode 100644
index 0000000..27f46c2
--- /dev/null
+++ b/src/main/java/eu/dissco/core/datacitepublisher/domain/RecoveryEvent.java
@@ -0,0 +1,10 @@
+package eu.dissco.core.datacitepublisher.domain;
+
+import java.util.List;
+
+public record RecoveryEvent(
+ List handles,
+ EventType eventType
+) {
+
+}
diff --git a/src/main/java/eu/dissco/core/datacitepublisher/exceptions/HandleResolutionException.java b/src/main/java/eu/dissco/core/datacitepublisher/exceptions/HandleResolutionException.java
new file mode 100644
index 0000000..034f2bc
--- /dev/null
+++ b/src/main/java/eu/dissco/core/datacitepublisher/exceptions/HandleResolutionException.java
@@ -0,0 +1,9 @@
+package eu.dissco.core.datacitepublisher.exceptions;
+
+public class HandleResolutionException extends Exception {
+
+ public HandleResolutionException(){
+ super();
+ }
+
+}
diff --git a/src/main/java/eu/dissco/core/datacitepublisher/properties/HandleConnectionProperties.java b/src/main/java/eu/dissco/core/datacitepublisher/properties/HandleConnectionProperties.java
new file mode 100644
index 0000000..cfff136
--- /dev/null
+++ b/src/main/java/eu/dissco/core/datacitepublisher/properties/HandleConnectionProperties.java
@@ -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;
+
+}
diff --git a/src/main/java/eu/dissco/core/datacitepublisher/security/JwtAuthConverter.java b/src/main/java/eu/dissco/core/datacitepublisher/security/JwtAuthConverter.java
new file mode 100644
index 0000000..376fc34
--- /dev/null
+++ b/src/main/java/eu/dissco/core/datacitepublisher/security/JwtAuthConverter.java
@@ -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 {
+ private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
+
+ @Override
+ public AbstractAuthenticationToken convert(@NotNull Jwt jwt) {
+ Collection authorities =
+ converterToStream(jwt).collect(Collectors.toSet());
+ return new JwtAuthenticationToken(jwt, authorities, getPrincipalClaimName(jwt));
+ }
+
+ private Stream converterToStream(Jwt jwt){
+ return Optional.of(jwtGrantedAuthoritiesConverter.convert(jwt)).stream()
+ .flatMap(Collection::stream);
+ }
+
+ private String getPrincipalClaimName(Jwt jwt) {
+ return jwt.getClaim(JwtClaimNames.SUB);
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/eu/dissco/core/datacitepublisher/security/MethodSecurityConfig.java b/src/main/java/eu/dissco/core/datacitepublisher/security/MethodSecurityConfig.java
new file mode 100644
index 0000000..c8c667c
--- /dev/null
+++ b/src/main/java/eu/dissco/core/datacitepublisher/security/MethodSecurityConfig.java
@@ -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();
+ }
+
+}
+
diff --git a/src/main/java/eu/dissco/core/datacitepublisher/security/WebSecurityConfig.java b/src/main/java/eu/dissco/core/datacitepublisher/security/WebSecurityConfig.java
new file mode 100644
index 0000000..30cab82
--- /dev/null
+++ b/src/main/java/eu/dissco/core/datacitepublisher/security/WebSecurityConfig.java
@@ -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();
+ }
+
+}
diff --git a/src/main/java/eu/dissco/core/datacitepublisher/service/RecoveryService.java b/src/main/java/eu/dissco/core/datacitepublisher/service/RecoveryService.java
new file mode 100644
index 0000000..7fb23e5
--- /dev/null
+++ b/src/main/java/eu/dissco/core/datacitepublisher/service/RecoveryService.java
@@ -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 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));
+ }
+
+}
diff --git a/src/main/java/eu/dissco/core/datacitepublisher/web/DataCiteClient.java b/src/main/java/eu/dissco/core/datacitepublisher/web/DataCiteClient.java
index f155776..c5035a8 100644
--- a/src/main/java/eu/dissco/core/datacitepublisher/web/DataCiteClient.java
+++ b/src/main/java/eu/dissco/core/datacitepublisher/web/DataCiteClient.java
@@ -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;
@@ -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 {
diff --git a/src/main/java/eu/dissco/core/datacitepublisher/web/HandleClient.java b/src/main/java/eu/dissco/core/datacitepublisher/web/HandleClient.java
new file mode 100644
index 0000000..0c6c230
--- /dev/null
+++ b/src/main/java/eu/dissco/core/datacitepublisher/web/HandleClient.java
@@ -0,0 +1,50 @@
+package eu.dissco.core.datacitepublisher.web;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import eu.dissco.core.datacitepublisher.exceptions.DataCiteApiException;
+import eu.dissco.core.datacitepublisher.exceptions.HandleResolutionException;
+import java.time.Duration;
+import java.util.List;
+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.client.WebClient;
+import reactor.util.retry.Retry;
+
+@RequiredArgsConstructor
+@Component
+@Slf4j
+public class HandleClient {
+
+ @Qualifier(value = "handle")
+ private final WebClient webClient;
+
+ public JsonNode resolveHandles(List handles) throws HandleResolutionException {
+ var response = webClient.method(HttpMethod.GET)
+ .uri(uriBuilder -> uriBuilder
+ .queryParam("handles", handles)
+ .build())
+ .retrieve()
+ .bodyToMono(JsonNode.class)
+ .retryWhen(
+ Retry.fixedDelay(3, Duration.ofSeconds(2))
+ .filter(WebClientUtils::is5xxServerError)
+ .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> new DataCiteApiException(
+ "External Service failed to process after max retries")));
+ try {
+ return response.toFuture().get();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ log.error("An Interrupted Exception has occurred in communicating with the Handle Manager API.", e);
+ throw new HandleResolutionException();
+ } catch (ExecutionException e) {
+ log.error("An execution Exception with the Handle API has occurred", e);
+ throw new HandleResolutionException();
+ }
+
+ }
+
+}
diff --git a/src/test/java/eu/dissco/core/datacitepublisher/TestUtils.java b/src/test/java/eu/dissco/core/datacitepublisher/TestUtils.java
index 59232a3..3ca25c5 100644
--- a/src/test/java/eu/dissco/core/datacitepublisher/TestUtils.java
+++ b/src/test/java/eu/dissco/core/datacitepublisher/TestUtils.java
@@ -9,6 +9,9 @@
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import eu.dissco.core.datacitepublisher.configuration.InstantDeserializer;
import eu.dissco.core.datacitepublisher.configuration.InstantSerializer;
+import eu.dissco.core.datacitepublisher.domain.EventType;
+import eu.dissco.core.datacitepublisher.domain.FdoType;
+import eu.dissco.core.datacitepublisher.domain.RecoveryEvent;
import eu.dissco.core.datacitepublisher.domain.datacite.DataCiteConstants;
import eu.dissco.core.datacitepublisher.domain.datacite.DcAlternateIdentifier;
import eu.dissco.core.datacitepublisher.domain.datacite.DcAttributes;
@@ -43,6 +46,8 @@ private TestUtils() {
public static final String PREFIX = "10.3535";
public static final String PID = "https://doi.org/" + PREFIX + "/" + SUFFIX;
public static final String DOI = PREFIX + "/" + SUFFIX;
+ public static final String DOI_ALT = PREFIX + "/2RL-RRS-4BX";
+ public static final String PID_ALT = "https://doi.org/" + DOI_ALT;
public static final String ROR = "https://ror.org/0566bfb96";
public static final String HOST_NAME = "Naturalis Biodiversity Center";
public static final String REFERENT_NAME = "New digital object";
@@ -73,7 +78,11 @@ private TestUtils() {
XML_MAPPER = new XmlMapper();
}
- public static DcAttributes givenSpecimenAttributes(String doi) {
+ public static DcAttributes givenSpecimenDataCiteAttributes(){
+ return givenSpecimenDataCiteAttributes(DOI);
+ }
+
+ public static DcAttributes givenSpecimenDataCiteAttributes(String doi) {
return DcAttributes.builder()
.suffix(SUFFIX)
.doi(doi)
@@ -107,17 +116,13 @@ public static DcAttributes givenSpecimenAttributes(String doi) {
.build();
}
- public static DcType givenType(String resourceType){
+ public static DcType givenType(String resourceType) {
return DcType.builder()
.resourceType(resourceType)
.build();
}
- public static DcAttributes givenSpecimenAttributes() {
- return givenSpecimenAttributes(DOI);
- }
-
- public static DcAttributes givenSpecimenAttributesFull() {
+ public static DcAttributes givenSpecimenDataCiteAttributesFull() {
return DcAttributes.builder()
.suffix(SUFFIX)
.doi(DOI)
@@ -166,7 +171,11 @@ public static DcAttributes givenSpecimenAttributesFull() {
}
public static JsonNode givenSpecimenJson() {
- return MAPPER.valueToTree(givenSpecimenAttributes());
+ return MAPPER.valueToTree(givenSpecimenDataCiteAttributes());
+ }
+
+ public static JsonNode givenSpecimenJson(String doi) {
+ return MAPPER.valueToTree(givenSpecimenDataCiteAttributes(doi));
}
public static DcAttributes givenMediaAttributes() {
@@ -282,10 +291,14 @@ private static List givenMediaDescriptionFull() {
);
}
- public static DigitalSpecimen givenDigitalSpecimen() {
+ public static DigitalSpecimen givenDigitalSpecimen(){
+ return givenDigitalSpecimen(PID);
+ }
+
+ public static DigitalSpecimen givenDigitalSpecimen(String doi) {
return new DigitalSpecimen()
.with10320Loc(LOCS)
- .withPid(PID)
+ .withPid(doi)
.withIssuedForAgent(ROR)
.withIssuedForAgentName(HOST_NAME)
.withPidRecordIssueDate(PID_ISSUE_DATE)
@@ -304,9 +317,13 @@ public static DigitalSpecimen givenDigitalSpecimenFull() {
}
public static MediaObject givenMediaObject() {
+ return givenMediaObject(PID);
+ }
+
+ public static MediaObject givenMediaObject(String pid) {
return new MediaObject()
.with10320Loc(LOCS)
- .withPid(PID)
+ .withPid(pid)
.withIssuedForAgent(ROR)
.withIssuedForAgentName(HOST_NAME)
.withPidRecordIssueDate(PID_ISSUE_DATE)
@@ -317,11 +334,57 @@ public static MediaObject givenMediaObject() {
.withLinkedDigitalObjectType(LinkedDigitalObjectType.DIGITAL_SPECIMEN);
}
+
public static MediaObject givenMediaObjectFull() {
return givenMediaObject()
.withMediaFormat(MediaFormat.IMAGE);
}
+ public static RecoveryEvent givenRecoveryEvent() {
+ return new RecoveryEvent(List.of(DOI, DOI_ALT), EventType.CREATE);
+ }
+
+ public static JsonNode givenDigitalSpecimenPidRecord() {
+ var specimen1 = MAPPER.createObjectNode()
+ .put("type", FdoType.DIGITAL_SPECIMEN.toString())
+ .set("attributes", MAPPER.valueToTree(givenDigitalSpecimen()));
+ var specimen2 = MAPPER.createObjectNode()
+ .put("type", FdoType.DIGITAL_SPECIMEN.toString())
+ .set("attributes", MAPPER.valueToTree(givenDigitalSpecimen(PID_ALT)));
+
+ return MAPPER.createObjectNode()
+ .put("links", "https://dev.dissco.tech/api/v1/pids/records")
+ .set("data", MAPPER.createArrayNode()
+ .add(specimen1)
+ .add(specimen2));
+ }
+
+ public static JsonNode givenDigitalSpecimenPidRecordSingle(String pid) {
+ var specimen1 = MAPPER.createObjectNode()
+ .put("type", FdoType.DIGITAL_SPECIMEN.toString())
+ .set("attributes", MAPPER.valueToTree(givenDigitalSpecimen(pid)));
+
+ return MAPPER.createObjectNode()
+ .put("links", "https://dev.dissco.tech/api/v1/pids/records")
+ .set("data", MAPPER.createArrayNode()
+ .add(specimen1));
+ }
+
+ public static JsonNode givenMediaObjectJson() {
+ var media1 = MAPPER.createObjectNode()
+ .put("type", FdoType.MEDIA_OBJECT.toString())
+ .set("attributes", MAPPER.valueToTree(givenMediaObject()));
+ var media2 = MAPPER.createObjectNode()
+ .put("type", FdoType.MEDIA_OBJECT.toString())
+ .set("attributes", MAPPER.valueToTree(givenMediaObject(PID_ALT)));
+
+ return MAPPER.createObjectNode()
+ .put("links", "https://dev.dissco.tech/api/v1/pids/records")
+ .set("data", MAPPER.createArrayNode()
+ .add(media1)
+ .add(media2));
+ }
+
public static DcNameIdentifiers givenIdentifier() {
return DcNameIdentifiers
diff --git a/src/test/java/eu/dissco/core/datacitepublisher/controller/RecoveryControllerTest.java b/src/test/java/eu/dissco/core/datacitepublisher/controller/RecoveryControllerTest.java
new file mode 100644
index 0000000..d746852
--- /dev/null
+++ b/src/test/java/eu/dissco/core/datacitepublisher/controller/RecoveryControllerTest.java
@@ -0,0 +1,38 @@
+package eu.dissco.core.datacitepublisher.controller;
+
+import static eu.dissco.core.datacitepublisher.TestUtils.givenRecoveryEvent;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import eu.dissco.core.datacitepublisher.service.RecoveryService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.HttpStatus;
+
+@ExtendWith(MockitoExtension.class)
+class RecoveryControllerTest {
+
+ @Mock
+ private RecoveryService recoveryService;
+
+ private RecoveryController recoveryController;
+
+ @BeforeEach
+ void init(){
+ recoveryController = new RecoveryController(recoveryService);
+ }
+
+ @Test
+ void testRecoverPids() throws Exception {
+ // Given
+
+ // When
+ var result = recoveryController.recoverPids(givenRecoveryEvent());
+
+ // Then
+ assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
+ }
+
+}
diff --git a/src/test/java/eu/dissco/core/datacitepublisher/domain/FdoTypeTest.java b/src/test/java/eu/dissco/core/datacitepublisher/domain/FdoTypeTest.java
new file mode 100644
index 0000000..5b93e59
--- /dev/null
+++ b/src/test/java/eu/dissco/core/datacitepublisher/domain/FdoTypeTest.java
@@ -0,0 +1,34 @@
+package eu.dissco.core.datacitepublisher.domain;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+class FdoTypeTest {
+
+ @Test
+ void testDsFromString(){
+ // When
+ var result = FdoType.fromString("https://hdl.handle.net/21.T11148/894b1e6cad57e921764e");
+
+ // Then
+ assertThat(result).isEqualTo(FdoType.DIGITAL_SPECIMEN);
+ }
+
+ @Test
+ void testMoFromString(){
+ // When
+ var result = FdoType.fromString("https://hdl.handle.net/21.T11148/bbad8c4e101e8af01115");
+
+ // Then
+ assertThat(result).isEqualTo(FdoType.MEDIA_OBJECT);
+ }
+
+ @Test
+ void testBadTypeFromString(){
+ // Then
+ assertThrows(IllegalStateException.class, () -> FdoType.fromString("Bad type"));
+ }
+
+}
diff --git a/src/test/java/eu/dissco/core/datacitepublisher/service/DataCitePublisherServiceTest.java b/src/test/java/eu/dissco/core/datacitepublisher/service/DataCitePublisherServiceTest.java
index 9276565..52cc192 100644
--- a/src/test/java/eu/dissco/core/datacitepublisher/service/DataCitePublisherServiceTest.java
+++ b/src/test/java/eu/dissco/core/datacitepublisher/service/DataCitePublisherServiceTest.java
@@ -13,14 +13,15 @@
import static eu.dissco.core.datacitepublisher.TestUtils.givenMediaAttributesFull;
import static eu.dissco.core.datacitepublisher.TestUtils.givenMediaObject;
import static eu.dissco.core.datacitepublisher.TestUtils.givenMediaObjectFull;
-import static eu.dissco.core.datacitepublisher.TestUtils.givenSpecimenAttributes;
-import static eu.dissco.core.datacitepublisher.TestUtils.givenSpecimenAttributesFull;
+import static eu.dissco.core.datacitepublisher.TestUtils.givenSpecimenDataCiteAttributes;
+import static eu.dissco.core.datacitepublisher.TestUtils.givenSpecimenDataCiteAttributesFull;
import static eu.dissco.core.datacitepublisher.TestUtils.givenType;
import static org.junit.Assert.assertThrows;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.lenient;
+import eu.dissco.core.datacitepublisher.TestUtils;
import eu.dissco.core.datacitepublisher.component.XmlLocReader;
import eu.dissco.core.datacitepublisher.domain.DigitalSpecimenEvent;
import eu.dissco.core.datacitepublisher.domain.EventType;
@@ -69,7 +70,7 @@ void testHandleDigitalSpecimenMessage() throws Exception {
var expected = MAPPER.valueToTree(
DcRequest.builder()
.data(DcData.builder()
- .attributes(givenSpecimenAttributes())
+ .attributes(givenSpecimenDataCiteAttributes())
.build())
.build());
given(xmlLocReader.getLocationsFromXml(LOCS)).willReturn(LOCS_ARR);
@@ -88,7 +89,7 @@ void testHandleDigitalSpecimenMessageUpdate() throws Exception {
var expected = MAPPER.valueToTree(
DcRequest.builder()
.data(DcData.builder()
- .attributes(givenSpecimenAttributes())
+ .attributes(givenSpecimenDataCiteAttributes())
.build())
.build());
given(xmlLocReader.getLocationsFromXml(LOCS)).willReturn(LOCS_ARR);
@@ -107,7 +108,7 @@ void testHandleDigitalSpecimenApiException() throws Exception {
var requestBody = MAPPER.valueToTree(
DcRequest.builder()
.data(DcData.builder()
- .attributes(givenSpecimenAttributes())
+ .attributes(givenSpecimenDataCiteAttributes())
.build())
.build());
given(xmlLocReader.getLocationsFromXml(LOCS)).willReturn(LOCS_ARR);
@@ -125,7 +126,7 @@ void testHandleDigitalSpecimenMessageFull() throws Exception {
var expected = MAPPER.valueToTree(
DcRequest.builder()
.data(DcData.builder()
- .attributes(givenSpecimenAttributesFull())
+ .attributes(givenSpecimenDataCiteAttributesFull())
.build())
.build());
given(xmlLocReader.getLocationsFromXml(LOCS)).willReturn(LOCS_ARR);
@@ -193,7 +194,7 @@ void testHandleDigitalSpecimenMessageTestEnv() throws Exception {
var expected = MAPPER.valueToTree(
DcRequest.builder()
.data(DcData.builder()
- .attributes(givenSpecimenAttributes(PREFIX + "/" + SUFFIX))
+ .attributes(TestUtils.givenSpecimenDataCiteAttributes(PREFIX + "/" + SUFFIX))
.build())
.build());
given(xmlLocReader.getLocationsFromXml(LOCS)).willReturn(LOCS_ARR);
diff --git a/src/test/java/eu/dissco/core/datacitepublisher/service/RecoveryServiceTest.java b/src/test/java/eu/dissco/core/datacitepublisher/service/RecoveryServiceTest.java
new file mode 100644
index 0000000..394e0b8
--- /dev/null
+++ b/src/test/java/eu/dissco/core/datacitepublisher/service/RecoveryServiceTest.java
@@ -0,0 +1,133 @@
+package eu.dissco.core.datacitepublisher.service;
+
+import static eu.dissco.core.datacitepublisher.TestUtils.DOI;
+import static eu.dissco.core.datacitepublisher.TestUtils.DOI_ALT;
+import static eu.dissco.core.datacitepublisher.TestUtils.MAPPER;
+import static eu.dissco.core.datacitepublisher.TestUtils.PID;
+import static eu.dissco.core.datacitepublisher.TestUtils.PID_ALT;
+import static eu.dissco.core.datacitepublisher.TestUtils.givenDigitalSpecimen;
+import static eu.dissco.core.datacitepublisher.TestUtils.givenDigitalSpecimenPidRecordSingle;
+import static eu.dissco.core.datacitepublisher.TestUtils.givenMediaObject;
+import static eu.dissco.core.datacitepublisher.TestUtils.givenMediaObjectJson;
+import static eu.dissco.core.datacitepublisher.TestUtils.givenRecoveryEvent;
+import static eu.dissco.core.datacitepublisher.TestUtils.givenDigitalSpecimenPidRecord;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.then;
+
+import eu.dissco.core.datacitepublisher.domain.DigitalSpecimenEvent;
+import eu.dissco.core.datacitepublisher.domain.EventType;
+import eu.dissco.core.datacitepublisher.domain.MediaObjectEvent;
+import eu.dissco.core.datacitepublisher.exceptions.HandleResolutionException;
+import eu.dissco.core.datacitepublisher.properties.HandleConnectionProperties;
+import eu.dissco.core.datacitepublisher.web.HandleClient;
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class RecoveryServiceTest {
+
+ @Mock
+ private HandleClient handleClient;
+ @Mock
+ private DataCitePublisherService dataCitePublisherService;
+ @Mock
+ private HandleConnectionProperties handleConnectionProperties;
+
+ private RecoveryService recoveryService;
+
+ @BeforeEach
+ void init() {
+ recoveryService = new RecoveryService(handleClient, dataCitePublisherService, MAPPER, handleConnectionProperties);
+ }
+
+ @Test
+ void testRecoverDoisSpecimen() throws Exception {
+ // Given
+ given(handleClient.resolveHandles(List.of(DOI, DOI_ALT)))
+ .willReturn(givenDigitalSpecimenPidRecord());
+ given(handleConnectionProperties.getMaxHandles()).willReturn(10);
+
+ // When
+ recoveryService.recoverDataciteDois(givenRecoveryEvent());
+
+ // Then
+ then(dataCitePublisherService).should()
+ .handleMessages(new DigitalSpecimenEvent(givenDigitalSpecimen(), EventType.CREATE));
+ then(dataCitePublisherService).should()
+ .handleMessages(new DigitalSpecimenEvent(givenDigitalSpecimen(PID_ALT), EventType.CREATE));
+ }
+
+ @Test
+ void testRecoverDoisSpecimenTwoPages() throws Exception {
+ // Given
+ given(handleClient.resolveHandles(List.of(DOI)))
+ .willReturn(givenDigitalSpecimenPidRecordSingle(PID));
+ given(handleClient.resolveHandles(List.of(DOI_ALT)))
+ .willReturn(givenDigitalSpecimenPidRecordSingle(PID_ALT));
+ given(handleConnectionProperties.getMaxHandles()).willReturn(1);
+
+ // When
+ recoveryService.recoverDataciteDois(givenRecoveryEvent());
+
+ // Then
+ then(dataCitePublisherService).should()
+ .handleMessages(new DigitalSpecimenEvent(givenDigitalSpecimen(), EventType.CREATE));
+ then(dataCitePublisherService).should()
+ .handleMessages(new DigitalSpecimenEvent(givenDigitalSpecimen(PID_ALT), EventType.CREATE));
+ }
+
+ @Test
+ void testRecoverDoisMedia() throws Exception {
+ // Given
+ given(handleClient.resolveHandles(List.of(DOI, DOI_ALT)))
+ .willReturn(givenMediaObjectJson());
+ given(handleConnectionProperties.getMaxHandles()).willReturn(10);
+
+ // When
+ recoveryService.recoverDataciteDois(givenRecoveryEvent());
+
+ // Then
+ then(dataCitePublisherService).should()
+ .handleMessages(new MediaObjectEvent(givenMediaObject(), EventType.CREATE));
+ then(dataCitePublisherService).should()
+ .handleMessages(new MediaObjectEvent(givenMediaObject(PID_ALT), EventType.CREATE));
+ }
+
+ @Test
+ void testRecoverDoisMissingData() throws Exception {
+ // Given
+ var handleMessage = MAPPER.readTree("""
+ {
+ "links":"https://dev.dissco.tech/api/v1/pids/records"
+ }
+ """);
+ given(handleClient.resolveHandles(anyList())).willReturn(handleMessage);
+ given(handleConnectionProperties.getMaxHandles()).willReturn(10);
+
+ // Then
+ assertThrows(HandleResolutionException.class, () -> recoveryService.recoverDataciteDois(givenRecoveryEvent()));
+ }
+
+ @Test
+ void testRecoverDoisDataNotArray() throws Exception {
+ // Given
+ var handleMessage = MAPPER.readTree("""
+ {
+ "links":"https://dev.dissco.tech/api/v1/pids/records",
+ "data": "yep"
+ }
+ """);
+ given(handleClient.resolveHandles(anyList())).willReturn(handleMessage);
+ given(handleConnectionProperties.getMaxHandles()).willReturn(10);
+
+ // Then
+ assertThrows(HandleResolutionException.class, () -> recoveryService.recoverDataciteDois(givenRecoveryEvent()));
+ }
+
+}
diff --git a/src/test/java/eu/dissco/core/datacitepublisher/web/HandleClientTest.java b/src/test/java/eu/dissco/core/datacitepublisher/web/HandleClientTest.java
new file mode 100644
index 0000000..6e55749
--- /dev/null
+++ b/src/test/java/eu/dissco/core/datacitepublisher/web/HandleClientTest.java
@@ -0,0 +1,100 @@
+package eu.dissco.core.datacitepublisher.web;
+
+import static eu.dissco.core.datacitepublisher.TestUtils.DOI;
+import static eu.dissco.core.datacitepublisher.TestUtils.DOI_ALT;
+import static eu.dissco.core.datacitepublisher.TestUtils.MAPPER;
+import static eu.dissco.core.datacitepublisher.TestUtils.givenDigitalSpecimenPidRecord;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import eu.dissco.core.datacitepublisher.exceptions.HandleResolutionException;
+import java.io.IOException;
+import java.util.List;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
+import org.springframework.web.reactive.function.client.WebClient;
+
+@ExtendWith(MockitoExtension.class)
+class HandleClientTest {
+
+ private static MockWebServer mockHandleServer;
+ private HandleClient handleClient;
+
+ @BeforeAll
+ static void init() throws IOException {
+ mockHandleServer = new MockWebServer();
+ mockHandleServer.start();
+ }
+
+
+ @BeforeEach
+ void setup() {
+ ExchangeFilterFunction errorResponseFilter = ExchangeFilterFunction
+ .ofResponseProcessor(WebClientUtils::exchangeFilterResponseProcessor);
+ var client = WebClient.builder()
+ .baseUrl(String.format("http://%s:%s", mockHandleServer.getHostName(),
+ mockHandleServer.getPort()))
+ .filter(errorResponseFilter)
+ .build();
+ handleClient = new HandleClient(client);
+ }
+
+ @AfterAll
+ static void destroy() throws IOException {
+ mockHandleServer.shutdown();
+ }
+
+ @Test
+ void testResolveHandle() throws Exception {
+ //
+ mockHandleServer.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.value())
+ .setBody(MAPPER.writeValueAsString(givenDigitalSpecimenPidRecord()))
+ .addHeader("Content-Type", "application/json"));
+
+ // When
+ var response = handleClient.resolveHandles(List.of(DOI, DOI_ALT));
+
+ // Then
+ assertThat(response).isEqualTo(givenDigitalSpecimenPidRecord());
+ }
+
+ @Test
+ void testRetries(){
+ // Given
+ int requestCount = mockHandleServer.getRequestCount();
+ mockHandleServer.enqueue(new MockResponse().setResponseCode(501));
+ mockHandleServer.enqueue(new MockResponse().setResponseCode(501));
+ mockHandleServer.enqueue(new MockResponse().setResponseCode(501));
+ mockHandleServer.enqueue(new MockResponse().setResponseCode(501));
+
+ // When
+ assertThrows(HandleResolutionException.class,
+ () -> handleClient.resolveHandles(List.of(DOI)));
+ var newRequestCount = mockHandleServer.getRequestCount();
+
+ // Then
+ assertThat(newRequestCount - requestCount).isEqualTo(4);
+ }
+
+ @Test
+ void testInterruptedException() {
+ // Given
+ mockHandleServer.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.value())
+ .addHeader("Content-Type", "application/json"));
+
+ Thread.currentThread().interrupt();
+
+ // When / Then
+ assertThrows(HandleResolutionException.class,
+ () -> handleClient.resolveHandles(List.of(DOI)));
+ }
+
+}