diff --git a/servicetalk-data-jackson-jersey/src/main/java/io/servicetalk/data/jackson/jersey/JacksonSerializerMessageBodyReaderWriter.java b/servicetalk-data-jackson-jersey/src/main/java/io/servicetalk/data/jackson/jersey/JacksonSerializerMessageBodyReaderWriter.java index 0eb86376b8..2a7a7c4dac 100644 --- a/servicetalk-data-jackson-jersey/src/main/java/io/servicetalk/data/jackson/jersey/JacksonSerializerMessageBodyReaderWriter.java +++ b/servicetalk-data-jackson-jersey/src/main/java/io/servicetalk/data/jackson/jersey/JacksonSerializerMessageBodyReaderWriter.java @@ -19,9 +19,11 @@ import io.servicetalk.buffer.api.BufferAllocator; import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; +import io.servicetalk.data.jackson.JacksonSerializationProvider; import io.servicetalk.data.jackson.JacksonSerializerFactory; import io.servicetalk.http.router.jersey.internal.SourceWrappers.PublisherSource; import io.servicetalk.http.router.jersey.internal.SourceWrappers.SingleSource; +import io.servicetalk.serialization.api.DefaultSerializer; import io.servicetalk.serializer.api.Deserializer; import io.servicetalk.serializer.api.SerializationException; import io.servicetalk.serializer.api.Serializer; @@ -93,29 +95,65 @@ public boolean isReadable(final Class type, final Type genericType, final Ann public Object readFrom(final Class type, final Type genericType, final Annotation[] annotations, final MediaType mediaType, final MultivaluedMap httpHeaders, final InputStream entityStream) throws WebApplicationException { - final JacksonSerializerFactory serializerFactory = getJacksonSerializerFactory(mediaType); + final boolean oldJacksonProviderPresent = + providers.getContextResolver(JacksonSerializationProvider.class, mediaType) != null; + + // FIXME 0.43 - Remove this branch when deprecation go away. + if (oldJacksonProviderPresent) { + return readFromOld(type, genericType, annotations, mediaType, httpHeaders, entityStream); + } else { + final JacksonSerializerFactory serializerFactory = getJacksonSerializerFactory(mediaType); + final ExecutionContext executionContext = ctxRefProvider.get().get().executionContext(); + final BufferAllocator allocator = executionContext.bufferAllocator(); + final int contentLength = requestCtxProvider.get().getLength(); + + if (Single.class.isAssignableFrom(type)) { + return handleEntityStream(entityStream, allocator, + (p, a) -> deserialize(p, serializerFactory.serializerDeserializer(getSourceClass(genericType)), + contentLength, a), + (is, a) -> new SingleSource<>(deserialize(toBufferPublisher(is, a), + serializerFactory.serializerDeserializer( + getSourceClass(genericType)), contentLength, a))); + } else if (Publisher.class.isAssignableFrom(type)) { + return handleEntityStream(entityStream, allocator, + (p, a) -> serializerFactory.streamingSerializerDeserializer( + getSourceClass(genericType)).deserialize(p, a), + (is, a) -> new PublisherSource<>(serializerFactory.streamingSerializerDeserializer( + getSourceClass(genericType)).deserialize(toBufferPublisher(is, a), a))); + } + + return handleEntityStream(entityStream, allocator, + (p, a) -> deserializeObject(p, serializerFactory.serializerDeserializer(type), contentLength, a), + (is, a) -> deserializeObject(toBufferPublisher(is, a), + serializerFactory.serializerDeserializer(type), contentLength, a)); + } + } + + // FIXME 0.43 - Remove this branch when deprecation go away. + @Deprecated + private Object readFromOld(final Class type, final Type genericType, final Annotation[] annotations, + final MediaType mediaType, final MultivaluedMap httpHeaders, + final InputStream entityStream) throws WebApplicationException { + final io.servicetalk.serialization.api.Serializer serializer = getSerializer(mediaType); final ExecutionContext executionContext = ctxRefProvider.get().get().executionContext(); final BufferAllocator allocator = executionContext.bufferAllocator(); final int contentLength = requestCtxProvider.get().getLength(); if (Single.class.isAssignableFrom(type)) { return handleEntityStream(entityStream, allocator, - (p, a) -> deserialize(p, serializerFactory.serializerDeserializer(getSourceClass(genericType)), - contentLength, a), - (is, a) -> new SingleSource<>(deserialize(toBufferPublisher(is, a), - serializerFactory.serializerDeserializer(getSourceClass(genericType)), contentLength, a))); + (p, a) -> deserializeOld(p, serializer, getSourceClass(genericType), contentLength, a), + (is, a) -> new SingleSource<>(deserializeOld(toBufferPublisher(is, a), serializer, + getSourceClass(genericType), contentLength, a))); } else if (Publisher.class.isAssignableFrom(type)) { return handleEntityStream(entityStream, allocator, - (p, a) -> serializerFactory.streamingSerializerDeserializer( - getSourceClass(genericType)).deserialize(p, a), - (is, a) -> new PublisherSource<>(serializerFactory.streamingSerializerDeserializer( - getSourceClass(genericType)).deserialize(toBufferPublisher(is, a), a))); + (p, a) -> serializer.deserialize(p, getSourceClass(genericType)), + (is, a) -> new PublisherSource<>(serializer.deserialize(toBufferPublisher(is, a), + getSourceClass(genericType)))); } return handleEntityStream(entityStream, allocator, - (p, a) -> deserializeObject(p, serializerFactory.serializerDeserializer(type), contentLength, a), - (is, a) -> deserializeObject(toBufferPublisher(is, a), serializerFactory.serializerDeserializer(type), - contentLength, a)); + (p, a) -> deserializeObjectOld(p, serializer, type, contentLength, a), + (is, a) -> deserializeObjectOld(toBufferPublisher(is, a), serializer, type, contentLength, a)); } @Override @@ -124,11 +162,45 @@ public boolean isWriteable(final Class type, final Type genericType, final An return !isSse(requestCtxProvider.get()) && isSupportedMediaType(mediaType); } + // FIXME 0.43 - Remove deprecation + @Deprecated + private void writeToOld(final Object o, final Class type, final Type genericType, final Annotation[] annotations, + final MediaType mediaType, final MultivaluedMap httpHeaders, + final OutputStream entityStream) throws WebApplicationException { + + final Publisher bufferPublisher; + if (o instanceof Single) { + bufferPublisher = getResponseBufferPublisher(((Single) o).toPublisher(), genericType, mediaType); + } else if (o instanceof Publisher) { + bufferPublisher = getResponseBufferPublisher((Publisher) o, genericType, mediaType); + } else { + bufferPublisher = getResponseBufferPublisher(Publisher.from(o), o.getClass(), mediaType); + } + + setResponseBufferPublisher(bufferPublisher, requestCtxProvider.get()); + } + + @SuppressWarnings("unchecked") + private Publisher getResponseBufferPublisher(final Publisher publisher, final Type type, + final MediaType mediaType) { + final BufferAllocator allocator = ctxRefProvider.get().get().executionContext().bufferAllocator(); + return getSerializer(mediaType).serialize(publisher, allocator, + type instanceof Class ? (Class) type : getSourceClass(type)); + } + @SuppressWarnings({"rawtypes", "unchecked"}) @Override public void writeTo(final Object o, final Class type, final Type genericType, final Annotation[] annotations, final MediaType mediaType, final MultivaluedMap httpHeaders, final OutputStream entityStream) throws WebApplicationException { + final boolean oldJacksonProviderPresent = + providers.getContextResolver(JacksonSerializationProvider.class, mediaType) != null; + if (oldJacksonProviderPresent) { + // FIXME 0.43 - Remove deprecation + writeToOld(o, type, genericType, annotations, mediaType, httpHeaders, entityStream); + return; + } + final BufferAllocator allocator = ctxRefProvider.get().get().executionContext().bufferAllocator(); final Publisher bufferPublisher; if (o instanceof Single) { @@ -160,6 +232,49 @@ private static Publisher toBufferPublisher(final InputStream is, final B return fromInputStream(is).map(a::wrap); } + // FIXME: 0.43 - Remove deprecations + @Deprecated + private io.servicetalk.serialization.api.Serializer getSerializer(final MediaType mediaType) { + return new DefaultSerializer(getOldJacksonSerializer(mediaType)); + } + + @Deprecated + private static T deserializeObjectOld(final Publisher bufferPublisher, + final io.servicetalk.serialization.api.Serializer ser, + final Class type, final int contentLength, + final BufferAllocator allocator) { + return awaitResult(deserializeOld(bufferPublisher, ser, type, contentLength, allocator).toFuture()); + } + + private JacksonSerializationProvider getOldJacksonSerializer(final MediaType mediaType) { + final ContextResolver contextResolver = + providers.getContextResolver(JacksonSerializationProvider.class, mediaType); + + return contextResolver.getContext(JacksonSerializationProvider.class); + } + + @Deprecated + private static Single deserializeOld(final Publisher bufferPublisher, + final io.servicetalk.serialization.api.Serializer ser, + final Class type, final int contentLength, + final BufferAllocator allocator) { + + return bufferPublisher + .collect(() -> newBufferForRequestContent(contentLength, allocator), Buffer::writeBytes) + .map(buf -> { + try { + return ser.deserializeAggregatedSingle(buf, type); + } catch (final NoSuchElementException e) { + throw new BadRequestException("No deserializable JSON content", e); + } catch (final io.servicetalk.serialization.api.SerializationException e) { + // SerializationExceptionMapper can't always tell for sure that the exception was thrown because + // of bad user data: here we are deserializing user data so we can assume we fail because of it + // and immediately throw the properly mapped JAX-RS exception + throw new BadRequestException("Invalid JSON data", e); + } + }); + } + private static Single deserialize( final Publisher bufferPublisher, final Deserializer deserializer, final int contentLength, final BufferAllocator allocator) { diff --git a/servicetalk-data-jackson-jersey/src/test/java/io/servicetalk/data/jackson/jersey/CustomJacksonSerializationFactoryTest.java b/servicetalk-data-jackson-jersey/src/test/java/io/servicetalk/data/jackson/jersey/CustomJacksonSerializationFactoryTest.java new file mode 100644 index 0000000000..eeb7479093 --- /dev/null +++ b/servicetalk-data-jackson-jersey/src/test/java/io/servicetalk/data/jackson/jersey/CustomJacksonSerializationFactoryTest.java @@ -0,0 +1,76 @@ +/* + * Copyright © 2018 Apple Inc. and the ServiceTalk project authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 io.servicetalk.data.jackson.jersey; + +import io.servicetalk.data.jackson.jersey.resources.SingleJsonResources; +import io.servicetalk.http.router.jersey.AbstractJerseyStreamingHttpServiceTest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import java.util.Map; +import java.util.Set; +import javax.ws.rs.core.Application; + +import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; +import static io.servicetalk.data.jackson.jersey.ServiceTalkJacksonSerializerFeature.ST_JSON_FEATURE; +import static io.servicetalk.data.jackson.jersey.ServiceTalkJacksonSerializerFeature.newContextResolver; +import static io.servicetalk.data.jackson.jersey.resources.SingleJsonResources.PATH; +import static io.servicetalk.http.api.HttpHeaderValues.APPLICATION_JSON; +import static io.servicetalk.http.api.HttpResponseStatus.OK; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonMap; +import static net.javacrumbs.jsonunit.JsonMatchers.jsonEquals; +import static org.glassfish.jersey.internal.InternalProperties.JSON_FEATURE; + +class CustomJacksonSerializationFactoryTest extends AbstractJerseyStreamingHttpServiceTest { + + static class TestApplication extends Application { + @Override + public Set> getClasses() { + return singleton(SingleJsonResources.class); + } + + @Override + public Set getSingletons() { + return singleton(newContextResolver(new ObjectMapper().disable(FAIL_ON_UNKNOWN_PROPERTIES))); + } + + @Override + public Map getProperties() { + return singletonMap(JSON_FEATURE, ST_JSON_FEATURE); + } + } + + @Override + protected Application application() { + return new TestApplication(); + } + + @Override + protected String testUri(final String path) { + return PATH + path; + } + + @ParameterizedTest + @EnumSource(RouterApi.class) + void postInvalidJsonPojo(final RouterApi api) throws Exception { + setUp(api); + sendAndAssertResponse(post("/pojo", "{\"foo\":\"bar\"}", APPLICATION_JSON), + OK, APPLICATION_JSON, jsonEquals("{\"aString\":\"nullx\",\"anInt\":1}"), __ -> null); + } +} diff --git a/servicetalk-data-jackson-jersey/src/test/java/io/servicetalk/data/jackson/jersey/CustomJacksonSerializationProviderTest.java b/servicetalk-data-jackson-jersey/src/test/java/io/servicetalk/data/jackson/jersey/CustomJacksonSerializationProviderTest.java index 96ece136ef..4bcb05f253 100644 --- a/servicetalk-data-jackson-jersey/src/test/java/io/servicetalk/data/jackson/jersey/CustomJacksonSerializationProviderTest.java +++ b/servicetalk-data-jackson-jersey/src/test/java/io/servicetalk/data/jackson/jersey/CustomJacksonSerializationProviderTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2018 Apple Inc. and the ServiceTalk project authors + * Copyright © 2022 Apple Inc. and the ServiceTalk project authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; import static io.servicetalk.data.jackson.jersey.ServiceTalkJacksonSerializerFeature.ST_JSON_FEATURE; -import static io.servicetalk.data.jackson.jersey.ServiceTalkJacksonSerializerFeature.newContextResolver; +import static io.servicetalk.data.jackson.jersey.ServiceTalkJacksonSerializerFeature.contextResolverFor; import static io.servicetalk.data.jackson.jersey.resources.SingleJsonResources.PATH; import static io.servicetalk.http.api.HttpHeaderValues.APPLICATION_JSON; import static io.servicetalk.http.api.HttpResponseStatus.OK; @@ -37,6 +37,8 @@ import static net.javacrumbs.jsonunit.JsonMatchers.jsonEquals; import static org.glassfish.jersey.internal.InternalProperties.JSON_FEATURE; +// FIXME 0.43 - Remove deprecation +@Deprecated class CustomJacksonSerializationProviderTest extends AbstractJerseyStreamingHttpServiceTest { static class TestApplication extends Application { @@ -47,7 +49,7 @@ public Set> getClasses() { @Override public Set getSingletons() { - return singleton(newContextResolver(new ObjectMapper().disable(FAIL_ON_UNKNOWN_PROPERTIES))); + return singleton(contextResolverFor(new ObjectMapper().disable(FAIL_ON_UNKNOWN_PROPERTIES))); } @Override