diff --git a/src/main/java/eu/enmeshed/ConnectorClient.java b/src/main/java/eu/enmeshed/ConnectorClient.java index 5dc61ff..6db8c98 100644 --- a/src/main/java/eu/enmeshed/ConnectorClient.java +++ b/src/main/java/eu/enmeshed/ConnectorClient.java @@ -1,10 +1,6 @@ package eu.enmeshed; -import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.util.StdDateFormat; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import eu.enmeshed.endpoints.AccountEndpoint; import eu.enmeshed.endpoints.AttributesEndpoint; import eu.enmeshed.endpoints.ChallengesEndpoint; @@ -15,6 +11,7 @@ import eu.enmeshed.endpoints.RelationshipTemplatesEndpoint; import eu.enmeshed.endpoints.RelationshipsEndpoint; import eu.enmeshed.endpoints.TokensEndpoint; +import eu.enmeshed.utils.CustomJsonMapperProvider; import feign.Feign; import feign.Logger.Level; import feign.Request.Options; @@ -25,13 +22,7 @@ @SuppressWarnings("ClassCanBeRecord") public class ConnectorClient { - private static final ObjectMapper objectMapper = - new ObjectMapper() - .registerModule(new JavaTimeModule()) - .setSerializationInclusion(Include.NON_ABSENT) - .disable(SerializationFeature.INDENT_OUTPUT) - .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) - .setDateFormat(new StdDateFormat().withColonInTimeZone(true)); + private static final ObjectMapper objectMapper = CustomJsonMapperProvider.createObjectMapper(); public final AccountEndpoint account; public final AttributesEndpoint attributes; @@ -80,7 +71,7 @@ public static ConnectorClient create(String url, String apiKey, Options options, .requestInterceptor(request -> request.header("X-API-KEY", apiKey)) .logLevel(loggerLevel) .options(options) - .errorDecoder(new ConnectorErrorDecoder()); + .errorDecoder(new ConnectorErrorDecoder(objectMapper)); return new ConnectorClient( AccountEndpoint.configure(url, builder), diff --git a/src/main/java/eu/enmeshed/ConnectorError.java b/src/main/java/eu/enmeshed/ConnectorError.java index cc55b81..5774b98 100644 --- a/src/main/java/eu/enmeshed/ConnectorError.java +++ b/src/main/java/eu/enmeshed/ConnectorError.java @@ -4,8 +4,12 @@ public record ConnectorError(String id, String code, String message, String docs, String time, String details, String[] stacktrace) { - Exception toException() { - return new Exception(message); + public boolean isRelationshipStatusWrong() { + return code.equals("error.consumption.requests.wrongRelationshipStatus"); + } + + public boolean hasPeerDeletionError() { + return code.equals("error.consumption.requests.peerIsInDeletion") || code.equals("error.consumption.requests.peerIsDeleted"); } @Override diff --git a/src/main/java/eu/enmeshed/ConnectorErrorDecoder.java b/src/main/java/eu/enmeshed/ConnectorErrorDecoder.java index 3a86f69..833adce 100644 --- a/src/main/java/eu/enmeshed/ConnectorErrorDecoder.java +++ b/src/main/java/eu/enmeshed/ConnectorErrorDecoder.java @@ -1,6 +1,9 @@ package eu.enmeshed; import com.fasterxml.jackson.databind.ObjectMapper; +import eu.enmeshed.exceptions.PeerDeletionException; +import eu.enmeshed.exceptions.WrongRelationshipStatusException; +import feign.FeignException; import feign.Request; import feign.Response; import feign.RetryableException; @@ -12,25 +15,35 @@ @Slf4j public class ConnectorErrorDecoder implements ErrorDecoder { - private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper; + + public ConnectorErrorDecoder(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } @Override - public Exception decode(String methodKey, Response response) { + public FeignException decode(String methodKey, Response response) { log.info("Response error with status {} and reason {}", response.status(), response.reason()); int responseStatus = response.status(); String responseReason = response.reason(); - log.info( - "Throw the RetryableException from a response error with status {} and reason {}", - responseStatus, - responseReason); + try (InputStream inputStream = response.body().asInputStream()) { + + byte[] responseBodyBytes = inputStream.readAllBytes(); + String responseBody = new String(responseBodyBytes); + ConnectorError connectorError = objectMapper.convertValue(objectMapper.readTree(responseBody).get("error"), ConnectorError.class); + + FeignException feignException = FeignException.errorStatus(methodKey, response); + + if (connectorError.isRelationshipStatusWrong()) { + return new WrongRelationshipStatusException(connectorError.message(), feignException.request(), responseBodyBytes, feignException.responseHeaders()); + } - try (InputStream bodyIs = response.body().asInputStream()) { - ConnectorErrorWrapper wrapper = objectMapper.readValue(bodyIs, ConnectorErrorWrapper.class); - var error = wrapper.error(); - log.info(error.toString()); + if (connectorError.hasPeerDeletionError()) { + return new PeerDeletionException(connectorError.message(), feignException.request(), responseBodyBytes, feignException.responseHeaders()); + } - return error.toException(); + return feignException; } catch (IOException e) { log.error("Failed to parse error response body", e); return new RetryableException( diff --git a/src/main/java/eu/enmeshed/ConnectorErrorWrapper.java b/src/main/java/eu/enmeshed/ConnectorErrorWrapper.java deleted file mode 100644 index 728aa01..0000000 --- a/src/main/java/eu/enmeshed/ConnectorErrorWrapper.java +++ /dev/null @@ -1,5 +0,0 @@ -package eu.enmeshed; - -public record ConnectorErrorWrapper(ConnectorError error) { - -} diff --git a/src/main/java/eu/enmeshed/exceptions/PeerDeletionException.java b/src/main/java/eu/enmeshed/exceptions/PeerDeletionException.java new file mode 100644 index 0000000..c4a0046 --- /dev/null +++ b/src/main/java/eu/enmeshed/exceptions/PeerDeletionException.java @@ -0,0 +1,13 @@ +package eu.enmeshed.exceptions; + +import feign.FeignException.BadRequest; +import feign.Request; +import java.util.Collection; +import java.util.Map; + +public class PeerDeletionException extends BadRequest { + + public PeerDeletionException(String message, Request request, byte[] responseBodyByte, Map> headers) { + super(message, request, responseBodyByte, headers); + } +} diff --git a/src/main/java/eu/enmeshed/exceptions/WrongRelationshipStatusException.java b/src/main/java/eu/enmeshed/exceptions/WrongRelationshipStatusException.java new file mode 100644 index 0000000..0a0bfe1 --- /dev/null +++ b/src/main/java/eu/enmeshed/exceptions/WrongRelationshipStatusException.java @@ -0,0 +1,13 @@ +package eu.enmeshed.exceptions; + +import feign.FeignException.BadRequest; +import feign.Request; +import java.util.Collection; +import java.util.Map; + +public class WrongRelationshipStatusException extends BadRequest { + + public WrongRelationshipStatusException(String message, Request request, byte[] responseBodyByte, Map> headers) { + super(message, request, responseBodyByte, headers); + } +} diff --git a/src/main/java/eu/enmeshed/model/relationships/ConnectorRelationship.java b/src/main/java/eu/enmeshed/model/relationships/ConnectorRelationship.java index 7d9cc59..8a711ea 100644 --- a/src/main/java/eu/enmeshed/model/relationships/ConnectorRelationship.java +++ b/src/main/java/eu/enmeshed/model/relationships/ConnectorRelationship.java @@ -31,9 +31,10 @@ public class ConnectorRelationship implements WebhookData { @Data public static class PeerDeletionInfo { - Status status; + String deletionDate; + DeletionStatus deletionStatus; - public enum Status { + public enum DeletionStatus { @JsonProperty("ToBeDeleted") TO_BE_DELETED, @JsonProperty("Deleted") diff --git a/src/main/java/eu/enmeshed/utils/CustomJsonMapperProvider.java b/src/main/java/eu/enmeshed/utils/CustomJsonMapperProvider.java new file mode 100644 index 0000000..f5718aa --- /dev/null +++ b/src/main/java/eu/enmeshed/utils/CustomJsonMapperProvider.java @@ -0,0 +1,42 @@ +package eu.enmeshed.utils; + +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.util.StdDateFormat; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.io.IOException; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CustomJsonMapperProvider { + + public static final String UNKNOWN_PROPERTY_LOG_FORMAT = "Unknown property '{}' encountered in JSON response for class '{}'."; + + public static ObjectMapper createObjectMapper() { + return JsonMapper.builder() + .addModule(new JavaTimeModule()) + .serializationInclusion(Include.NON_ABSENT) + .disable(SerializationFeature.INDENT_OUTPUT) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .defaultDateFormat(new StdDateFormat().withColonInTimeZone(true)) + .addHandler(new CustomDeserializationProblemHandler()) + .build(); + } + + private static class CustomDeserializationProblemHandler extends DeserializationProblemHandler { + + @Override + public boolean handleUnknownProperty(DeserializationContext ctxt, JsonParser p, JsonDeserializer deserializer, Object beanOrClass, String propertyName) throws IOException { + log.warn(UNKNOWN_PROPERTY_LOG_FORMAT, + propertyName, beanOrClass.getClass().getSimpleName()); + p.skipChildren(); + return true; + } + } +} diff --git a/src/test/java/eu/enmeshed/ConnectorErrorDecoderTest.java b/src/test/java/eu/enmeshed/ConnectorErrorDecoderTest.java new file mode 100644 index 0000000..ee6bdfa --- /dev/null +++ b/src/test/java/eu/enmeshed/ConnectorErrorDecoderTest.java @@ -0,0 +1,74 @@ +package eu.enmeshed; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.enmeshed.exceptions.PeerDeletionException; +import eu.enmeshed.exceptions.WrongRelationshipStatusException; +import feign.FeignException; +import feign.FeignException.BadRequest; +import feign.Request; +import feign.Response; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@Slf4j +class ConnectorErrorDecoderTest { + + private ConnectorErrorDecoder errorDecoder; + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + errorDecoder = new ConnectorErrorDecoder(objectMapper); // Inject the mock + } + + @Test + void shouldThrowWrongRelationshipStatusException() throws IOException { + String mockBody = "{\"error\": {\"message\": \"Relationship status is wrong\", \"code\": \"error.consumption.requests.wrongRelationshipStatus\"}}"; + + Response response = createMockResponse(400, mockBody); + FeignException exception = errorDecoder.decode("Post", response); + + assertInstanceOf(WrongRelationshipStatusException.class, exception); + assertEquals("Relationship status is wrong", exception.getMessage()); + } + + @Test + void shouldThrowPeerDeletionException() throws IOException { + String mockBody = "{\"error\": {\"message\": \"Peer is in deletion\", \"code\": \"error.consumption.requests.peerIsInDeletion\"}}"; + + Response response = createMockResponse(400, mockBody); + FeignException exception = errorDecoder.decode("Post", response); + + assertInstanceOf(PeerDeletionException.class, exception); + assertEquals("Peer is in deletion", exception.getMessage()); + } + + @Test + void shouldThrowBadRequestException() throws IOException { + String mockBody = "{\"error\": {\"message\": \"message\", \"code\": \"some other code\"}}"; + + Response response = createMockResponse(400, mockBody); + FeignException exception = errorDecoder.decode("Post", response); + + assertInstanceOf(BadRequest.class, exception); + } + + // Helper method to create a mock Feign Response + private Response createMockResponse(int status, String body) { + Request request = Request.create(Request.HttpMethod.GET, "/test", Collections.emptyMap(), null, null, null); + return Response.builder() + .status(status) + .reason("Some reason") + .request(request) + .body(body, StandardCharsets.UTF_8) + .build(); + } +} \ No newline at end of file diff --git a/src/test/java/eu/enmeshed/CustomJsonMapperProviderTest.java b/src/test/java/eu/enmeshed/CustomJsonMapperProviderTest.java new file mode 100644 index 0000000..ffbd078 --- /dev/null +++ b/src/test/java/eu/enmeshed/CustomJsonMapperProviderTest.java @@ -0,0 +1,39 @@ +package eu.enmeshed; + +import static org.hamcrest.MatcherAssert.assertThat; + +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.enmeshed.utils.CustomJsonMapperProvider; +import lombok.Getter; +import lombok.Setter; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class CustomJsonMapperProviderTest { + + @Setter + @Getter + private static class TestClass { + + private String knownProperty; + } + + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + objectMapper = CustomJsonMapperProvider.createObjectMapper(); + } + + @Test + void shouldMapKnownFieldsWithNoError() { + String json = "{ \"knownProperty\": \"value\", \"someProperty\": \"unexpected\" }"; + TestClass result = Assertions.assertDoesNotThrow(() -> + objectMapper.readValue(json, TestClass.class) + ); + assertThat(result, CoreMatchers.notNullValue()); + assertThat(result.getKnownProperty(), CoreMatchers.equalTo("value")); + } +} \ No newline at end of file