From 66ff867b3e703b1460dfe2140639210a1427d188 Mon Sep 17 00:00:00 2001 From: Nikhil Tummidi <66246330+nikhilT-d11@users.noreply.github.com> Date: Fri, 29 Mar 2024 15:45:15 +0530 Subject: [PATCH] Add custom objectMapper in JsonProvider (#12) * feat: add custom objectMapper in JsonProvider * chore: add test for extra keys in request body * chore: update provider registration * feat: make registerProvider through objects protected * feat: enable custom json providers through interface * chore: update readme * chore: update readme --- docs/index.md | 30 ++++++++++- .../dream11/rest/AbstractRestVerticle.java | 22 ++++++-- .../dream11/rest/provider/JsonProvider.java | 52 +----------------- .../rest/provider/impl/JacksonProvider.java | 54 +++++++++++++++++++ src/test/java/com/dream11/rest/RestApiIT.java | 12 +++++ .../dream11/rest/verticle/RestVerticle.java | 9 ++++ 6 files changed, 124 insertions(+), 55 deletions(-) create mode 100644 src/main/java/com/dream11/rest/provider/impl/JacksonProvider.java diff --git a/docs/index.md b/docs/index.md index df1f5bd..9e4f487 100644 --- a/docs/index.md +++ b/docs/index.md @@ -136,7 +136,35 @@ public class TestRoute { - Refer to [`com.dream11.rest.injector.GuiceInjector`](src/test/java/com/dream11/rest/injector/GuiceInjector.java) for an example ## Custom Providers -Annotate your provider classes with `@Provider` to automatically register them with resteasy deployment +- Annotate your provider classes with `@Provider` to automatically register them with resteasy deployment + +- Alternatively, if your provider class necessitates a constructor with parameters, you can override the following method in +[AbstractRestVerticle](src/main/java/com/dream11/rest/AbstractRestVerticle.java) to register the provider object +```java +public class RestVerticle extends AbstractRestVerticle { + + @Override + protected List getProviderObjects() { + List providerObjects = super.getProviderObjects(); + // add your provider object with custom constructor to the list + return providerObjects; + } +} +``` + +### Custom Json Provider +You can create custom json providers by +- Implement the `JsonProvider` interface, defined in [JsonProvider](src/main/java/com/dream11/rest/provider/JsonProvider.java) and then, +- Override the following method in [AbstractRestVerticle](src/main/java/com/dream11/rest/AbstractRestVerticle.java) +```java +public class RestVerticle extends AbstractRestVerticle { + + @Override + protected JsonProvider getJsonProvider() { + // return your custom json provider + } +} +``` ## Examples Please refer [tests](/src/test/java/com/dream11/rest) for an example application diff --git a/src/main/java/com/dream11/rest/AbstractRestVerticle.java b/src/main/java/com/dream11/rest/AbstractRestVerticle.java index 4154854..e55b053 100644 --- a/src/main/java/com/dream11/rest/AbstractRestVerticle.java +++ b/src/main/java/com/dream11/rest/AbstractRestVerticle.java @@ -8,10 +8,12 @@ import com.dream11.rest.filter.TimeoutFilter; import com.dream11.rest.provider.JsonProvider; import com.dream11.rest.provider.ParamConverterProvider; +import com.dream11.rest.provider.impl.JacksonProvider; import com.dream11.rest.util.AnnotationUtil; import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Single; import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.json.jackson.DatabindCodec; import io.vertx.rxjava3.core.AbstractVerticle; import io.vertx.rxjava3.core.Context; import io.vertx.rxjava3.core.RxHelper; @@ -56,6 +58,10 @@ protected RequestResponseFilter getReqResFilter() { return new LoggerFilter(); } + protected JsonProvider getJsonProvider() { + return new JacksonProvider(DatabindCodec.mapper()); + } + @Override public Completable rxStart() { return this.startHttpServer().doOnSuccess(server -> this.httpServer = server).ignoreElement(); @@ -116,23 +122,33 @@ protected VertxResteasyDeployment buildResteasyDeployment() { deployment.start(); List> routes = AnnotationUtil.getClassesWithAnnotation(packageName, Path.class); log.info("JAX-RS routes : " + routes.size()); - ResteasyProviderFactory resteasyProviderFactory = deployment.getProviderFactory(); - this.getProviders().forEach(resteasyProviderFactory::register); + this.registerProviders(deployment.getProviderFactory()); + // not using deployment.getRegistry().addPerInstanceResource because it creates new instance of resource for each request routes.forEach(route -> deployment.getRegistry().addSingletonResource(this.getInjector().getInstance(route))); return deployment; } + private void registerProviders(ResteasyProviderFactory resteasyProviderFactory) { + this.getProviderObjects().forEach(resteasyProviderFactory::register); + this.getProviders().forEach(resteasyProviderFactory::register); + } + protected List> getProviders() { List> providers = new ArrayList<>(); providers.add(TimeoutFilter.class); providers.add(ValidationExceptionMapper.class); providers.add(GenericExceptionMapper.class); providers.add(WebApplicationExceptionMapper.class); - providers.add(JsonProvider.class); providers.add(ParamConverterProvider.class); providers.add(this.getReqResFilter().getClass()); providers.addAll(AnnotationUtil.getClassesWithAnnotation(packageName, Provider.class)); return providers; } + + protected List getProviderObjects() { + List providerObjects = new ArrayList<>(); + providerObjects.add(this.getJsonProvider()); + return providerObjects; + } } diff --git a/src/main/java/com/dream11/rest/provider/JsonProvider.java b/src/main/java/com/dream11/rest/provider/JsonProvider.java index 267c40c..17bd6a3 100644 --- a/src/main/java/com/dream11/rest/provider/JsonProvider.java +++ b/src/main/java/com/dream11/rest/provider/JsonProvider.java @@ -1,53 +1,3 @@ package com.dream11.rest.provider; -import com.dream11.rest.annotation.TypeValidationError; -import com.dream11.rest.exception.RestException; -import com.fasterxml.jackson.databind.JsonMappingException; -import io.vertx.core.json.jackson.DatabindCodec; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.MultivaluedMap; -import jakarta.ws.rs.ext.Provider; -import java.io.IOException; -import java.io.InputStream; -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; -import java.util.List; -import lombok.SneakyThrows; -import org.apache.http.HttpStatus; -import org.jboss.resteasy.plugins.providers.jackson.ResteasyJackson2Provider; - -@Provider -@Consumes({"application/json", "application/*+json", "text/json"}) -@Produces({"application/json", "application/*+json", "text/json"}) -public class JsonProvider extends ResteasyJackson2Provider { - - public JsonProvider() { - this.setMapper(DatabindCodec.mapper()); - } - - @SneakyThrows - @Override - public Object readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, - MultivaluedMap httpHeaders, InputStream entityStream) { - try { - return super.readFrom(type, genericType, annotations, mediaType, httpHeaders, entityStream); - } catch (JsonMappingException e) { - List referenceList = e.getPath(); - if (!referenceList.isEmpty()) { - String fieldName = referenceList.get(0).getFieldName(); - if (fieldName != null) { - TypeValidationError typeValidationError = type.getDeclaredField(fieldName).getAnnotation(TypeValidationError.class); - if (typeValidationError != null) { - throw new RestException(typeValidationError.code(), typeValidationError.message(), - typeValidationError.httpStatusCode(), e); - } - } - } - throw new RestException("INVALID_REQUEST", e.getMessage(), HttpStatus.SC_BAD_REQUEST, e); - } catch (IOException e) { - throw new RestException("INVALID_REQUEST", e.getMessage(), HttpStatus.SC_BAD_REQUEST, e); - } - } -} +public interface JsonProvider {} diff --git a/src/main/java/com/dream11/rest/provider/impl/JacksonProvider.java b/src/main/java/com/dream11/rest/provider/impl/JacksonProvider.java new file mode 100644 index 0000000..aeaa2d0 --- /dev/null +++ b/src/main/java/com/dream11/rest/provider/impl/JacksonProvider.java @@ -0,0 +1,54 @@ +package com.dream11.rest.provider.impl; + +import com.dream11.rest.annotation.TypeValidationError; +import com.dream11.rest.exception.RestException; +import com.dream11.rest.provider.JsonProvider; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.Provider; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.List; +import lombok.SneakyThrows; +import org.apache.http.HttpStatus; +import org.jboss.resteasy.plugins.providers.jackson.ResteasyJackson2Provider; + +@Provider +@Consumes({"application/json", "application/*+json", "text/json"}) +@Produces({"application/json", "application/*+json", "text/json"}) +public class JacksonProvider extends ResteasyJackson2Provider implements JsonProvider { + + public JacksonProvider(ObjectMapper mapper) { + this.setMapper(mapper); + } + + @SneakyThrows + @Override + public Object readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, InputStream entityStream) { + try { + return super.readFrom(type, genericType, annotations, mediaType, httpHeaders, entityStream); + } catch (JsonMappingException e) { + List referenceList = e.getPath(); + if (!referenceList.isEmpty()) { + String fieldName = referenceList.get(0).getFieldName(); + if (fieldName != null) { + TypeValidationError typeValidationError = type.getDeclaredField(fieldName).getAnnotation(TypeValidationError.class); + if (typeValidationError != null) { + throw new RestException(typeValidationError.code(), typeValidationError.message(), + typeValidationError.httpStatusCode(), e); + } + } + } + throw new RestException("INVALID_REQUEST", e.getMessage(), HttpStatus.SC_BAD_REQUEST, e); + } catch (IOException e) { + throw new RestException("INVALID_REQUEST", e.getMessage(), HttpStatus.SC_BAD_REQUEST, e); + } + } +} diff --git a/src/test/java/com/dream11/rest/RestApiIT.java b/src/test/java/com/dream11/rest/RestApiIT.java index 4cb8f97..9d41ec5 100644 --- a/src/test/java/com/dream11/rest/RestApiIT.java +++ b/src/test/java/com/dream11/rest/RestApiIT.java @@ -108,6 +108,18 @@ void positiveBodyTest() { assertThat(response.statusCode()).isEqualTo(200); } + @Test + void extraFieldsTest() { + // arrange + String path = String.format("%s", Constants.VALIDATION_ROUTE_PATH); + JsonObject body = new JsonObject().put("resourceId", "1").put("extraKey", "extraValue"); + // act + HttpResponse response = this.makePostRequest(path, body) + .blockingGet(); + // assert + assertThat(response.statusCode()).isEqualTo(200); + } + @Test void routeNotFoundTest() { // arrange diff --git a/src/test/java/com/dream11/rest/verticle/RestVerticle.java b/src/test/java/com/dream11/rest/verticle/RestVerticle.java index b7392cc..6803db4 100644 --- a/src/test/java/com/dream11/rest/verticle/RestVerticle.java +++ b/src/test/java/com/dream11/rest/verticle/RestVerticle.java @@ -5,7 +5,11 @@ import com.dream11.rest.ClassInjector; import com.dream11.rest.Constants; import com.dream11.rest.injector.GuiceInjector; +import com.dream11.rest.provider.JsonProvider; +import com.dream11.rest.provider.impl.JacksonProvider; import com.dream11.rest.util.SharedDataUtil; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; import io.vertx.core.http.HttpServerOptions; public class RestVerticle extends AbstractRestVerticle { @@ -18,4 +22,9 @@ public RestVerticle() { protected ClassInjector getInjector() { return SharedDataUtil.getInstance(this.vertx.getDelegate(), GuiceInjector.class); } + + @Override + protected JsonProvider getJsonProvider() { + return new JacksonProvider(new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)); + } }