Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 3 additions & 12 deletions src/main/java/eu/enmeshed/ConnectorClient.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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),
Expand Down
8 changes: 6 additions & 2 deletions src/main/java/eu/enmeshed/ConnectorError.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 24 additions & 11 deletions src/main/java/eu/enmeshed/ConnectorErrorDecoder.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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(
Expand Down
5 changes: 0 additions & 5 deletions src/main/java/eu/enmeshed/ConnectorErrorWrapper.java

This file was deleted.

13 changes: 13 additions & 0 deletions src/main/java/eu/enmeshed/exceptions/PeerDeletionException.java
Original file line number Diff line number Diff line change
@@ -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<String, Collection<String>> headers) {
super(message, request, responseBodyByte, headers);
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Collection<String>> headers) {
super(message, request, responseBodyByte, headers);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
42 changes: 42 additions & 0 deletions src/main/java/eu/enmeshed/utils/CustomJsonMapperProvider.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
74 changes: 74 additions & 0 deletions src/test/java/eu/enmeshed/ConnectorErrorDecoderTest.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
39 changes: 39 additions & 0 deletions src/test/java/eu/enmeshed/CustomJsonMapperProviderTest.java
Original file line number Diff line number Diff line change
@@ -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"));
}
}
Loading