Skip to content
This repository has been archived by the owner on Dec 1, 2022. It is now read-only.

Commit

Permalink
Add JSON-RPC error/exception resolvers that support POJOs (#362)
Browse files Browse the repository at this point in the history
This adds Jackson-based (de)serialization for JSON-RPC servers and clients that will serialize/deserialize Throwable instances that have custom fields correctly.
  • Loading branch information
wjbuys authored Jan 30, 2018
2 parents defffaf 965b80f commit dcc212d
Show file tree
Hide file tree
Showing 29 changed files with 864 additions and 58 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 43,7 @@ subprojects {
}
dependencySet(group: 'org.slf4j', version: '1.7. ') {
entry 'slf4j-api'
entry 'slf4j-simple'
entry 'jcl-over-slf4j'
}
dependencySet(group: 'org.mapstruct', version: '1.2.0.CR2') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 14,8 @@
*/
package com.amazonaws.blox.dataservicemodel.v1.exception;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;

@Getter
Expand All @@ -22,7 24,10 @@ public class ResourceExistsException extends ClientException {
private String resourceType;
private String resourceId;

public ResourceExistsException(String resourceType, String resourceId) {
@JsonCreator
public ResourceExistsException(
@JsonProperty("resourceType") String resourceType,
@JsonProperty("resourceId") String resourceId) {
super(String.format("%s with id %s already exists", resourceType, resourceId));

this.resourceType = resourceType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 14,8 @@
*/
package com.amazonaws.blox.dataservicemodel.v1.exception;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;

@Getter
Expand All @@ -22,7 24,11 @@ public class ResourceInUseException extends ClientException {
private String resourceType;
private String resourceId;

public ResourceInUseException(String resourceType, String resourceId, String message) {
@JsonCreator
public ResourceInUseException(
@JsonProperty("resourceType") String resourceType,
@JsonProperty("resourceId") String resourceId,
@JsonProperty("message") String message) {
super(message);

this.resourceType = resourceType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 14,8 @@
*/
package com.amazonaws.blox.dataservicemodel.v1.exception;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;

@Getter
Expand All @@ -22,7 24,10 @@ public class ResourceNotFoundException extends ClientException {
private String resourceType;
private String resourceId;

public ResourceNotFoundException(String resourceType, String resourceId) {
@JsonCreator
public ResourceNotFoundException(
@JsonProperty("resourceType") String resourceType,
@JsonProperty("resourceId") String resourceId) {
super(String.format("%s with id %s could not be found", resourceType, resourceId));

this.resourceType = resourceType;
Expand Down
12 changes: 11 additions & 1 deletion integ-tests/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 17,11 @@ cucumberTest {
systemProperties = [
'aws.region': stack.region.toString(),
'aws.defaultProfile': stack.profile.toString(),

// Suppress the Spring Context's verbose startup logs that usualy get
// emitted on each scenario run, but still warn when things go wrong.
// If you want to debug Spring issues, set this to info or debug.
'org.slf4j.simpleLogger.log.org.springframework': 'warn',
]
}

Expand All @@ -27,7 32,12 @@ dependencies {
'com.google.guava:guava:23.0',
'info.cukes:cucumber-java8:1.2.5',
'info.cukes:cucumber-spring:1.2.5',
// To use JUnit assertions in the step definitions:

// Wire the Spring Framework's JCL logs into SLF4J
'org.slf4j:jcl-over-slf4j',
// Use the SLF4J simpleLogger for logging in tests
'org.slf4j:slf4j-simple',

'junit:junit:4.12',
'org.assertj:assertj-core:3.8 ',

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 19,6 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import com.amazonaws.blox.dataservicemodel.v1.exception.ResourceExistsException;
import com.amazonaws.blox.dataservicemodel.v1.exception.ResourceInUseException;
import com.amazonaws.blox.dataservicemodel.v1.exception.ResourceNotFoundException;
import com.amazonaws.blox.dataservicemodel.v1.model.Cluster;
import com.amazonaws.blox.dataservicemodel.v1.model.Environment;
import com.amazonaws.blox.dataservicemodel.v1.model.EnvironmentHealth;
Expand Down Expand Up @@ -194,46 191,24 @@ public DataServiceSteps() {
Then(
"^there should be an? \"?\'?(\\w*)\"?\'? thrown$",
(final String exceptionName) -> {
assertNotNull("Expecting an exception to be thrown", exceptionContext.getException());
assertEquals(exceptionName, exceptionContext.getException().getClass().getSimpleName());
assertThat(exceptionContext.getException())
.isNotNull()
.satisfies(t -> assertThat(t.getClass().getSimpleName()).isEqualTo(exceptionName));
});

And(
"^the resourceType is \"([^\"]*)\"$",
(final String resourceType) -> {
assertNotNull("Expecting an exception to be thrown", exceptionContext.getException());
if (exceptionContext.getException() instanceof ResourceNotFoundException) {
final ResourceNotFoundException exception =
(ResourceNotFoundException) exceptionContext.getException();
assertEquals(resourceType, exception.getResourceType());
} else if (exceptionContext.getException() instanceof ResourceExistsException) {
final ResourceExistsException exception =
(ResourceExistsException) exceptionContext.getException();
assertEquals(resourceType, exception.getResourceType());
} else if (exceptionContext.getException() instanceof ResourceInUseException) {
final ResourceInUseException exception =
(ResourceInUseException) exceptionContext.getException();
assertEquals(resourceType, exception.getResourceType());
}
"^its ([^ ]*) is \"([^\"]*)\"$",
(final String field, final String value) -> {
assertThat(exceptionContext.getException()).hasFieldOrPropertyWithValue(field, value);
});

And(
"^the resourceId contains \"([^\"]*)\"$",
(final String resourceId) -> {
assertNotNull("Expecting an exception to be thrown", exceptionContext.getException());
if (exceptionContext.getException() instanceof ResourceNotFoundException) {
final ResourceNotFoundException exception =
(ResourceNotFoundException) exceptionContext.getException();
assertEquals(resourceId, exception.getResourceType());
} else if (exceptionContext.getException() instanceof ResourceExistsException) {
final ResourceExistsException exception =
(ResourceExistsException) exceptionContext.getException();
assertEquals(resourceId, exception.getResourceType());
} else if (exceptionContext.getException() instanceof ResourceInUseException) {
final ResourceInUseException exception =
(ResourceInUseException) exceptionContext.getException();
assertEquals(resourceId, exception.getResourceType());
}
"^its ([^ ]*) contains \"([^\"]*)\"$",
(final String field, final String value) -> {
assertThat(exceptionContext.getException())
.hasFieldOrProperty(field)
.extracting(field)
.allSatisfy(s -> assertThat(s).asString().contains(value));
});

When(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 15,15 @@
package cucumber.steps.helpers;

import java.util.function.Function;
import lombok.SneakyThrows;

@FunctionalInterface
public interface ThrowingFunction<T, R> extends Function<T, R> {

@Override
@SneakyThrows
default R apply(T t) {
try {
return applyThrows(t);
} catch (Exception e) {
throw new RuntimeException(e);
}
return applyThrows(t);
}

R applyThrows(T t) throws Exception;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 27,8 @@ public class MemoizedWrapper implements Memoized {
Multimaps.synchronizedListMultimap(ArrayListMultimap.create());

@Override
public <T> T getLastFromHistory(Class<T> type) {
@SuppressWarnings("unchecked")
public <T> T getLastFromHistory(final Class<T> type) {
return (T) memory.get(type).get(memory.get(type).size() - 1);
}

Expand All @@ -37,6 38,7 @@ public final <T> void addToHistory(final Class<T> type, final T value) {
}

@Override
@SuppressWarnings("unchecked")
public final <T, R> R memoizeFunction(final T input, final ThrowingFunction<T, R> fn) {
Validate.notNull(input);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 6,12 @@ Feature: Create environment
When I create an environment
Then the created environment response is valid

@ignore
Scenario: Create an environment that already exists
Given I create an environment named "test" in the cluster "testCluster"
When I try to create another environment with the name "test" in the cluster "testCluster"
Then there should be a ResourceExistsException thrown
And the resourceType is "environment"
And the resourceId contains "test"
And its resourceType is "environment"
And its resourceId contains "test"

Scenario: Create an environment that has the same name as another environment in a different cluster
Given I create an environment named "test"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 14,19 @@ Feature: Describe environment
When I describe the updated environment
Then the updated and described environments match

@ignore
Scenario: Describe a non-existent environment
When I try to describe a non-existent environment named "non-existent"
Then there should be a ResourceNotFoundException thrown
And the resourceType is "environment"
And the resourceId contains "non-existent"
And its resourceType is "environment"
And its resourceId contains "non-existent"

@ignore
Scenario: Describe a deleted environment
Given I create an environment named "test"
And I delete the created environment
When I try to describe the created environment
Then there should be a ResourceNotFoundException thrown
And the resourceType is "environment"
And the resourceId contains "test"
And its resourceType is "environment"
And its resourceId contains "test"

#TODO: Add invalid parameter tests
18 changes: 18 additions & 0 deletions json-rpc-core/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 1,18 @@
plugins {
id "java"
}

group 'com.amazonaws.blox'
version '0.1-SNAPSHOT'

sourceCompatibility = 1.8

dependencies {
compile "com.github.briandilley.jsonrpc4j:jsonrpc4j: "
compile "org.projectlombok:lombok"

testCompile "org.slf4j:slf4j-simple"
testCompile "junit:junit:4.12"
testCompile "org.assertj:assertj-core:3.8 "
testCompileOnly "org.projectlombok:lombok"
}
Original file line number Diff line number Diff line change
@@ -0,0 1,62 @@
/*
* Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may
* not use this file except in compliance with the License. A copy of the
* License is located at
*
* http://aws.amazon.com/apache2.0/
*
* or in the "LICENSE" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.amazonaws.blox.jsonrpc;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.googlecode.jsonrpc4j.ErrorResolver;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class PojoErrorResolver implements ErrorResolver {
// Since we encode the type name, just use one error code for all errors that were encoded by this
// resolver. The number is arbitrary, and just chosen to be high enough to not accidentally
// conflict with another error code.
public static final int ERROR_CODE = 9001;

private final ObjectMapper mapper;

public PojoErrorResolver(final ObjectMapper mapper) {
this.mapper = mapper.copy().addMixIn(Throwable.class, ThrowableSerializationMixin.class);
}

@Override
public JsonError resolveError(
final Throwable t, final Method method, final List<JsonNode> arguments) {

final boolean isModeledException =
Arrays.stream(method.getExceptionTypes()).anyMatch(aClass -> aClass.isInstance(t));

if (isModeledException) {
// We have to explicitly serialize the exception type here using the mapper, rather than
// relying on the mapper serializing the entire JsonError object. This is needed because
// Jackson looks up the mixins based on the type of the *variable* not the object instance.
// Since the type of the data field on JsonError is just Object, it then doesn't correctly
// apply the annotations from ThrowableSerializationMixin.
final JsonNode node = mapper.valueToTree(t);
return new JsonError(ERROR_CODE, t.getMessage(), node);
}

log.warn(
"Exception type {} is not modeled in signature of {}, returning generic error",
t.getClass(),
method.getName());

return new JsonError(JsonError.INTERNAL_ERROR.code, t.getMessage(), null);
}
}
Loading

0 comments on commit dcc212d

Please sign in to comment.