Skip to content

Commit

Permalink
Fix jersey-jackson not considering deprecated JacksonSerializationPro…
Browse files Browse the repository at this point in the history
…vider #2060 (#2064)

Motivation:

Upon deprecation of JacksonSerializationProvider our servicetalk-data-jackson-jersey stopped evaluating custom object-mappers offered through the ServiceTalkJacksonSerializerFeature#contextResolverFor(ObjectMapper).

Modifications:

Restore old execution path if JacksonSerializationProvider is found as a resolver during runtime, otherwise fallback to the new flow.
Order is "old custom Jackson (if found)" -> "new custom Jackson (if found)" -> "new Jackson (fallback)"

Result:

Offer solid migration path while the deprecated JacksonSerializationProvider is still around.

(cherry picked from commit 0f0998b)
  • Loading branch information
tkountis authored Jan 26, 2022
1 parent 5b800ee commit e3b3c3c
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -93,29 +95,65 @@ public boolean isReadable(final Class<?> type, final Type genericType, final Ann
public Object readFrom(final Class<Object> type, final Type genericType, final Annotation[] annotations,
final MediaType mediaType, final MultivaluedMap<String, String> 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<Object> type, final Type genericType, final Annotation[] annotations,
final MediaType mediaType, final MultivaluedMap<String, String> 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
Expand All @@ -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<String, Object> httpHeaders,
final OutputStream entityStream) throws WebApplicationException {

final Publisher<Buffer> 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<Buffer> 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<String, Object> 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<Buffer> bufferPublisher;
if (o instanceof Single) {
Expand Down Expand Up @@ -160,6 +232,49 @@ private static Publisher<Buffer> 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> T deserializeObjectOld(final Publisher<Buffer> bufferPublisher,
final io.servicetalk.serialization.api.Serializer ser,
final Class<T> type, final int contentLength,
final BufferAllocator allocator) {
return awaitResult(deserializeOld(bufferPublisher, ser, type, contentLength, allocator).toFuture());
}

private JacksonSerializationProvider getOldJacksonSerializer(final MediaType mediaType) {
final ContextResolver<JacksonSerializationProvider> contextResolver =
providers.getContextResolver(JacksonSerializationProvider.class, mediaType);

return contextResolver.getContext(JacksonSerializationProvider.class);
}

@Deprecated
private static <T> Single<T> deserializeOld(final Publisher<Buffer> bufferPublisher,
final io.servicetalk.serialization.api.Serializer ser,
final Class<T> 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 <T> Single<T> deserialize(
final Publisher<Buffer> bufferPublisher, final Deserializer<T> deserializer, final int contentLength,
final BufferAllocator allocator) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Class<?>> getClasses() {
return singleton(SingleJsonResources.class);
}

@Override
public Set<Object> getSingletons() {
return singleton(newContextResolver(new ObjectMapper().disable(FAIL_ON_UNKNOWN_PROPERTIES)));
}

@Override
public Map<String, Object> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -47,7 +49,7 @@ public Set<Class<?>> getClasses() {

@Override
public Set<Object> getSingletons() {
return singleton(newContextResolver(new ObjectMapper().disable(FAIL_ON_UNKNOWN_PROPERTIES)));
return singleton(contextResolverFor(new ObjectMapper().disable(FAIL_ON_UNKNOWN_PROPERTIES)));
}

@Override
Expand Down

0 comments on commit e3b3c3c

Please sign in to comment.