Skip to content

Commit

Permalink
Add ExceptionMapper for RuntimeJsonException (#486)
Browse files Browse the repository at this point in the history
* Add RuntimeJsonExceptionMapper
* Extract duplicate code to package-private JsonExceptionMapper
* Add response assertions to JsonProcessingExceptionMapperTest 

Closes #478
  • Loading branch information
sleberknight authored Apr 30, 2024
1 parent 54332b4 commit fbf794b
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.kiwiproject.dropwizard.util.exception;

import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import jakarta.ws.rs.core.Response;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import org.kiwiproject.jaxrs.exception.JaxrsBadRequestException;
import org.kiwiproject.jaxrs.exception.JaxrsException;
import org.kiwiproject.jaxrs.exception.JaxrsExceptionMapper;

@UtilityClass
@Slf4j
class JsonExceptionMappers {

static final String DEFAULT_MSG = "Unable to process JSON";

static Response toResponse(JsonProcessingException exception) {
JaxrsException e;

if (exception instanceof JsonGenerationException || exception instanceof InvalidDefinitionException) {
LOG.warn("Error generating JSON", exception);
e = new JaxrsException(exception);
} else {
var message = exception.getOriginalMessage();

if (message.startsWith("No suitable constructor found")) {
LOG.error("Unable to deserialize the specific type", exception);
e = new JaxrsException(exception);
} else {
LOG.debug(DEFAULT_MSG, exception);
e = new JaxrsBadRequestException(message, exception);
}
}

return JaxrsExceptionMapper.buildResponse(e);
}
}
Original file line number Diff line number Diff line change
@@ -1,45 +1,23 @@
package org.kiwiproject.dropwizard.util.exception;

import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import lombok.extern.slf4j.Slf4j;
import org.kiwiproject.jaxrs.exception.JaxrsBadRequestException;
import org.kiwiproject.jaxrs.exception.JaxrsException;
import org.kiwiproject.jaxrs.exception.JaxrsExceptionMapper;

/**
* Override default Dropwizard mapper to use kiwi's {@link org.kiwiproject.jaxrs.exception.ErrorMessage ErrorMessage}.
* The response entity is built using {@link JaxrsExceptionMapper#buildResponseEntity(JaxrsException)}.
*/
@Slf4j
@Provider
public class JsonProcessingExceptionMapper implements ExceptionMapper<JsonProcessingException> {

public static final String DEFAULT_MSG = "Unable to process JSON";
public static final String DEFAULT_MSG = JsonExceptionMappers.DEFAULT_MSG;

@Override
public Response toResponse(JsonProcessingException exception) {
JaxrsException e;

if (exception instanceof JsonGenerationException || exception instanceof InvalidDefinitionException) {
LOG.warn("Error generating JSON", exception);
e = new JaxrsException(exception);
} else {
var message = exception.getOriginalMessage();

if (message.startsWith("No suitable constructor found")) {
LOG.error("Unable to deserialize the specific type", exception);
e = new JaxrsException(exception);
} else {
LOG.debug(DEFAULT_MSG, exception);
e = new JaxrsBadRequestException(message, exception);
}
}

return JaxrsExceptionMapper.buildResponse(e);
return JsonExceptionMappers.toResponse(exception);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.kiwiproject.dropwizard.util.exception;

import com.fasterxml.jackson.core.JsonProcessingException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import lombok.extern.slf4j.Slf4j;
import org.kiwiproject.jaxrs.exception.JaxrsException;
import org.kiwiproject.jaxrs.exception.JaxrsExceptionMapper;
import org.kiwiproject.json.RuntimeJsonException;

/**
* Map {@link RuntimeJsonException} to {@link Response}.
* <p>
* If the cause of the {@link RuntimeJsonException} is a {@link JsonProcessingException} then
* the behavior is the same as {@link JsonProcessingExceptionMapper}. Otherwise, the mapped
* response is a 500 Internal Server Error.
* <p>
*/
@Slf4j
public class RuntimeJsonExceptionMapper implements ExceptionMapper<RuntimeJsonException> {

public static final String DEFAULT_MSG = JsonExceptionMappers.DEFAULT_MSG;

@Override
public Response toResponse(RuntimeJsonException runtimeJsonException) {
var throwable = runtimeJsonException.getCause();

if (throwable instanceof JsonProcessingException jsonProcessingException) {
return JsonExceptionMappers.toResponse(jsonProcessingException);
}

LOG.warn("Cause of RuntimeJsonException was not JsonProcessingException, but was {}. Returning a 500.",
throwable.getClass().getName());
return JaxrsExceptionMapper.buildResponse(JaxrsException.buildJaxrsException(throwable));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.kiwiproject.dropwizard.util.exception.ErrorMessageAssertions.assertAndGetErrorMessage;
import static org.kiwiproject.test.constants.KiwiTestConstants.OBJECT_MAPPER;
import static org.kiwiproject.test.jaxrs.JaxrsTestHelper.assertBadRequest;
import static org.kiwiproject.test.jaxrs.JaxrsTestHelper.assertInternalServerErrorResponse;
import static org.mockito.Mockito.mock;

import com.fasterxml.jackson.core.JsonGenerationException;
Expand All @@ -25,13 +27,20 @@ void shouldProcessJsonProcessingException() {
var mapper = new JsonProcessingExceptionMapper();
var jsonException = createJsonException();
var response = mapper.toResponse(jsonException);
var errorMessage = assertAndGetErrorMessage(response);
assertBadRequest(response);

var errorMessage = assertAndGetErrorMessage(response);
assertThat(errorMessage.getMessage()).isEqualTo(jsonException.getOriginalMessage());
}

private JsonProcessingException createJsonException() {
String badJson = "{ \"first\": \"Bob\" \"last\": \"Jones\" )"; // missing comma before 'last' property
// missing comma before 'last' property
var badJson = """
{
"first": "Bob"
"last": "Jones"
}
""";
try {
OBJECT_MAPPER.readValue(badJson, Person.class);
} catch (JsonProcessingException e) {
Expand All @@ -45,8 +54,9 @@ void shouldProcessJsonGenerationException() {
var mapper = new JsonProcessingExceptionMapper();
var jsonException = new JsonGenerationException("Problem generating", mock(JsonGenerator.class));
var response = mapper.toResponse(jsonException);
var errorMessage = assertAndGetErrorMessage(response);
assertInternalServerErrorResponse(response);

var errorMessage = assertAndGetErrorMessage(response);
assertThat(errorMessage.getMessage()).isEqualTo(jsonException.getOriginalMessage());
}

Expand All @@ -57,8 +67,9 @@ void shouldProcessInvalidDefinitionException() {
"Problem generating", mock(JavaType.class));

var response = mapper.toResponse(jsonException);
var errorMessage = assertAndGetErrorMessage(response);
assertInternalServerErrorResponse(response);

var errorMessage = assertAndGetErrorMessage(response);
assertThat(errorMessage.getMessage()).isEqualTo(jsonException.getOriginalMessage());
}

Expand All @@ -67,8 +78,9 @@ void shouldProcessJsonProcessingException_WithSpecificMessage() {
var mapper = new JsonProcessingExceptionMapper();
var jsonException = new JsonParseException(mock(JsonParser.class), "No suitable constructor found for Foo");
var response = mapper.toResponse(jsonException);
var errorMessage = assertAndGetErrorMessage(response);
assertInternalServerErrorResponse(response);

var errorMessage = assertAndGetErrorMessage(response);
assertThat(errorMessage.getMessage()).isEqualTo(jsonException.getOriginalMessage());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package org.kiwiproject.dropwizard.util.exception;

import static org.assertj.core.api.Assertions.assertThat;
import static org.kiwiproject.dropwizard.util.exception.ErrorMessageAssertions.assertAndGetErrorMessage;
import static org.kiwiproject.test.constants.KiwiTestConstants.JSON_HELPER;
import static org.kiwiproject.test.jaxrs.JaxrsTestHelper.assertBadRequest;
import static org.kiwiproject.test.jaxrs.JaxrsTestHelper.assertInternalServerErrorResponse;
import static org.mockito.Mockito.mock;

import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import lombok.Getter;
import lombok.Setter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.kiwiproject.json.RuntimeJsonException;

import java.io.IOException;

@DisplayName("RuntimeJsonExceptionMapper")
class RuntimeJsonExceptionMapperTest {

private RuntimeJsonExceptionMapper mapper;

@BeforeEach
void setUp() {
mapper = new RuntimeJsonExceptionMapper();
}

@Test
void shouldProcessExceptionHavingCauseOfJsonProcessingException() {
var runtimeJsonException = createRuntimeJsonException();
var jsonException = (JsonProcessingException) runtimeJsonException.getCause();
var response = mapper.toResponse(runtimeJsonException);
assertBadRequest(response);

var errorMessage = assertAndGetErrorMessage(response);
assertThat(errorMessage.getMessage()).isEqualTo(jsonException.getOriginalMessage());
}

private static RuntimeJsonException createRuntimeJsonException() {
// missing comma before 'last' property
var badJson = """
{
"first": "Bob"
"last": "Jones"
}
""";
try {
JSON_HELPER.toObject(badJson, Person.class);
} catch (RuntimeJsonException e) {
return e;
}
throw new RuntimeException("somehow didn't get a RuntimeJsonException parsing the bad JSON");
}

@Test
void shouldProcessExceptionHavingCauseOfJsonGenerationException() {
var jsonException = new JsonGenerationException("Problem generating", mock(JsonGenerator.class));
var runtimeJsonException = new RuntimeJsonException(jsonException);
var response = mapper.toResponse(runtimeJsonException);
assertInternalServerErrorResponse(response);

var errorMessage = assertAndGetErrorMessage(response);
assertThat(errorMessage.getMessage()).isEqualTo(jsonException.getOriginalMessage());
}

@Test
void shouldProcessExceptionHavingCauseOfInvalidDefinitionException() {
var jsonException = InvalidDefinitionException.from(mock(JsonParser.class),
"Problem generating", mock(JavaType.class));
var runtimeJsonException = new RuntimeJsonException(jsonException);

var response = mapper.toResponse(runtimeJsonException);
assertInternalServerErrorResponse(response);

var errorMessage = assertAndGetErrorMessage(response);
assertThat(errorMessage.getMessage()).isEqualTo(jsonException.getOriginalMessage());
}

@Test
void shouldProcessExceptionHavingCauseOfJsonProcessingException_WithSpecificMessage() {
var jsonException = new JsonParseException(mock(JsonParser.class), "No suitable constructor found for Foo");
var runtimeJsonException = new RuntimeJsonException(jsonException);

var response = mapper.toResponse(runtimeJsonException);
assertInternalServerErrorResponse(response);

var errorMessage = assertAndGetErrorMessage(response);
assertThat(errorMessage.getMessage()).isEqualTo(jsonException.getOriginalMessage());
}

@Test
void shouldProcessExceptionHavingCauseOfUnsupportedType() {
var ioException = new IOException("some weird character encoding problem...");
var runtimeJsonException = new RuntimeJsonException(ioException);
var response = mapper.toResponse(runtimeJsonException);
assertInternalServerErrorResponse(response);

var errorMessage = assertAndGetErrorMessage(response);
assertThat(errorMessage.getMessage()).isEqualTo(ioException.getMessage());
}

@Getter
@Setter
private static class Person {
private String first;
private String last;
}
}

0 comments on commit fbf794b

Please sign in to comment.